52 Commits
Author SHA1 Message Date
mcndave 11756bdfd2 - WIP on package manager addition 2026-07-16 14:14:19 -04:00
mcndave 00e5a0380a - Added some more flexibility to castValue method 2026-07-14 15:19:25 +00:00
mcndave e093495909 - Added DiExtension ; this part will soon be revamped 2026-07-07 12:23:49 +00:00
mcndave 9ddb93b847 - Added enums to select values 2026-07-01 12:54:53 -04:00
Dev 363da77669 - Fixed a bug when an enum is given as value 2026-06-09 19:33:35 +00:00
Dave Mc Nicoll 94903d6f05 - Added some checkup on constructor properties 2026-05-21 15:00:37 +00:00
Dave Mc Nicoll 3d412d6a46 - Defined sets() as chainable 2026-02-25 19:15:21 +00:00
Dev ca2d71e3ed - Added a messages() method 2026-02-05 21:22:46 +00:00
mcndave 7e2d4c7dc2 - Added formContextValuesRoot to form context 2026-01-16 16:21:50 -05:00
mcndave 2e3788a295 - Fixed CSRF missing from sent form 2025-11-06 17:17:45 +00:00
Dave Mc Nicoll 6e7a8f12fb - Added casting of values from FormContext set property 2025-10-27 18:33:09 +00:00
Dave Mc Nicoll a1c51bed10 - Added a ui:/form token to form/endform 2025-10-15 15:01:20 +00:00
Dave Mc Nicoll 1044e35946 - WIP on object initialization for FormContext from request body values 2025-10-14 18:18:31 +00:00
Dave Mc Nicoll 6ccf07825a - Added form context defined property 2025-09-15 20:04:49 +00:00
mcndave 23f24fef50 - Added form interface which connect to this context 2025-07-17 18:36:09 +00:00
Dave Mc Nicoll 33e77b5129 - Added getRequest() method to replace direct property access 2025-05-21 18:38:25 +00:00
Dave Mc Nicoll a34ebaf0df - Fixed executed() method and added a new method handling in UiForm 2024-11-01 16:12:57 -04:00
Dave Mc Nicoll 388b2a70f8 - UiTime older annotation was not properly setup 2024-10-14 12:55:02 +00:00
Dave Mc Nicoll 195586a078 - Added a new initialize() method launched after __construct() 2024-06-05 17:48:14 +00:00
Dave Mc Nicoll 5e9ad29f3c - Filling values of context if vars exists within it 2024-06-04 13:38:46 +00:00
Dave Mc Nicoll 1991122c32 - Filling values of context if vars exists within it 2024-06-04 13:34:31 +00:00
Dave Mc Nicoll 0a7b1cdc12 - Added request method to context 2024-05-31 12:28:34 +00:00
mcndave 804deb69a2 - Added PPH-DI definition file 2023-11-15 22:12:47 -05:00
Dave Mc Nicoll 328a4d282c - Added new methods for each inputs ui_...; fixed empty or 0 values checkbox/radio checked handling 2023-05-12 18:58:22 +00:00
Dave Mc Nicoll fd4e30bd42 Fixed 'img' tags, removed utf8_encode since it's deprecated and readded echoRaw from textarea widget 2023-04-13 15:51:29 -04:00
Dave Mc Nicoll ea780c697e - Fixed 'img' input which was missing 2023-03-29 11:31:45 -04:00
mcndave 6aaa05b37d - Refractoring on the old code base 2023-03-29 15:11:54 +00:00
mcndave 72940b3615 - Small quickfix adding backward compat. to the old ui.endform notation 2023-03-29 15:10:24 +00:00
mcndave aba66479be - Token name and options are now passed through parse() method 2023-02-02 19:38:20 +00:00
mcndave 77d05ae051 - Quickfix again 2023-02-02 18:18:05 +00:00
mcndave d984ddc540 - Fixed a bug added in previous commit 2023-02-02 18:16:28 +00:00
mcndave 5397ed4fae - First task of v2 done 2023-02-02 16:43:15 +00:00
mcndave 30ddd4c98b - Fixed some bugs related to options 2023-02-02 15:26:26 +00:00
mcndave f00c98b7ff - Some more work done on documentation and fixed some code at PHP 8 level 2023-01-26 13:33:53 +00:00
mcndave 3986437800 - Added documentation, fixed some bugs and optimized some parts of the source code while doing the doc. 2022-12-22 14:34:37 +00:00
mcndave 9190987b53 - Fixed the parse type 2022-12-21 04:24:12 +00:00
mcndave cd3cb18665 Merge branch 'master' of https://git.mcnd.ca/mcndave/picea-ui 2022-11-29 19:48:28 +00:00
mcndave 3a930c13b1 - Removed sprintf which was causing problems with it's own escaping mechanism while using it in a view 2022-11-29 19:48:17 +00:00
Dave Mc Nicoll bd501bce86 - Added the multiple value selection 2022-11-11 13:58:22 +00:00
Dave Mc Nicoll 1f5d38a8b9 - Added some more information into the form context whether a form was sent, and what was it's return value (if there was any) 2022-10-10 15:30:15 +00:00
Dave Mc Nicoll 164a27d267 - Added context validation status 2022-06-27 17:29:16 +00:00
Dave Mc Nicoll ceebc6783a - Added basic handling of BODY content from Web-RestMethod from PS 2022-05-11 14:09:18 +00:00
Dave Mc Nicoll 83fee32ac0 - Added CSRF through form processing 2022-02-25 15:33:40 +00:00
mcndave 97f1f67af1 - Implemented CSRF check, some bugfixes also included 2022-02-03 20:06:04 +00:00
Dave Mc Nicoll 362184f81f - Quickfix on attributes 2021-10-19 12:39:49 +00:00
mcndave 0d9db0470e - Worked with the files begin on the context part 2021-03-09 15:27:54 +00:00
mcndave e75d997b02 - Added a new UiImage element.
- Inputs can now have a class array OR a simple string as 'class' param.
2021-03-01 16:35:57 +00:00
mcndave 34828f642c Small bugfixe within the UiPopup Widget 2021-01-06 20:24:54 +00:00
mcndave 6020fa300d Merge branch 'master' of https://git.mcnd.ca/mcndave/picea-ui 2020-10-06 15:54:35 +00:00
mcndave 7437bbfc73 - Casted type to string for selection matching 2020-10-06 15:50:27 +00:00
Dave Mc Nicoll 95c30be9f8 Merge branch 'master' of https://git.mcnd.ca/mcndave/picea-ui 2020-10-06 11:40:53 -04:00
Dave Mc Nicoll d05a750b39 - Fixed Numeric types (number) 2020-10-06 11:40:27 -04:00
63 changed files with 1244 additions and 274 deletions

No files matched your search

+13 -4
View File
@@ -6,21 +6,30 @@
"authors": [
{
"name": "Dave Mc Nicoll",
"email": "mcndave@gmail.com"
"email": "info@mcnd.ca"
}
],
"require": {
"mcnd/picea": "dev-master"
"mcnd/picea": "^1.4.0"
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/mcNdave/picea.git"
"type": "composer",
"url": "https://git.mcnd.ca/api/packages/mcndave/composer"
}
],
"autoload": {
"psr-4": {
"Picea\\Ui\\": "src/"
}
},
"extra": {
"lean": {
"autoload": {
"definitions": [
"meta/definitions.php"
]
}
}
}
}
+127
View File
@@ -0,0 +1,127 @@
# Picea UI
Welcome to the `picea-ui` documentation.
This quick documentation will guide you through the use of the form system, every available
UI elements and how to add customs one and individual components.
Mostly everything you need to build and process form is available in this package.
The first part is most input callable as a widget :
```
{% ui:text "fullname", "John Doe" %}
```
Current input support is :
```php
- Input (string $source, array $attributes = [], array $options = [])
- Checkbox (string $name, mixed $value = null, array $attributes = [], array $options = [])
- Color (string $name, mixed $value = null, array $attributes = [], array $options = [])
- Date (string $name, mixed $value = null, array $attributes = [], array $options = [])
- Datetime (string $name, mixed $value = null, array $attributes = [], array $options = [])
- Email (string $name, mixed $value = null, array $attributes = [], array $options = [])
- File (string $name, mixed $value = null, array $attributes = [], array $options = [])
- Hidden (string $name, mixed $value = null, array $attributes = [], array $options = [])
- Image (string $source, array $attributes = [], array $options = [])
- Numeric (string $name, mixed $value = null, array $attributes = [], array $options = [])
- Password (string $name, mixed $value = null, array $attributes = [], array $options = [])
- Radio (string $name, mixed $value = null, array $attributes = [], array $options = [])
- Range (string $name, mixed $value = null, array $attributes = [], array $options = [])
- Search (string $name, mixed $value = null, array $attributes = [], array $options = [])
- Select (string $name, array $list, mixed $value = null, array $attributes = [], bool $strictComparison = true)
- Tel (string $name, mixed $value = null, array $attributes = [], array $options = [])
- Text (string $name, mixed $value = null, array $attributes = [], array $options = [])
- Textarea (string $name, mixed $value = null, array $attributes = [], array $options = [])
- Time (string $name, mixed $value = null, array $attributes = [], array $options = [])
- Url (string $name, mixed $value = null, array $attributes = [], array $options = [])
- Week (string $name, mixed $value = null, array $attributes = [], array $options = [])
```
**[PICEA]** So, using this code:
```html
{% ui:form.post "user.save", current_url() %}
<div class="field fullname">
<label>Full name</label>
{% ui:text "fullname", $user->fullname, [ 'required' => 'required' ] %}
</div>
<div class="field email">
<label>Email address</label>
{% ui:text "email", $user->email %}
</div>
<div class="field job">
<label>Have a job</label>
<label>{% ui:radio "job", 'yes', [], [ 'value' => $entity->job ] %} Yes</label>
<label>{% ui:radio "job", 'no', [], [ 'value' => $entity->job ] %} No</label>
</div>
<div class="field role">
<label>Role</label>
{% ui:select "role", [ 'user' => "User", 'moderator' => "Moderator", 'admin' => "Admin" ], $user->role, [ 'required' => 'required' ] %}
</div>
<button type="submit">Save</button>
{% ui:endform %}
```
**[HTML]** Would render as such:
```html
<form class="ui-form" method="post" action="http://127.0.0.1/" enctype="multipart/form-data">
<input class="ui-hidden" type="hidden" name="picea-ui-form[user.save]" value="617fb40f1999db63fbd0b4f680a77750">
<div class="field fullname">
<label>Full name</label>
<input class="ui-text" type="text" value="" name="fullname" required="required">
</div>
<div class="field email">
<label>Email address</label>
<input class="ui-text" type="text" value="" name="email">
</div>
<div class="field job">
<label>Have a job</label>
<label><input class="ui-radio" type="radio" value="yes" name="job"> Yes</label>
<label><input class="ui-radio" type="radio" value="no" name="job"> No</label>
</div>
<div class="field role">
<label>Role</label>
<select class="ui-select" name="role" required="required">
<option value="user">User</option>
<option value="moderator">Moderator</option>
<option value="admin">Admin</option>
</select>
</div>
<button type="submit">Save</button>
</form>
```
## Use your forms in controller
Forms can be used to handle how your data is handled, from loading (get) to processing (post).
The process is as simple as defining a Form, implementing FormInterface :
```php
interface FormInterface
{
public function initialize(FormContextInterface $context) : void;
public function validate(FormContextInterface $context) : bool;
public function execute(FormContextInterface $context);
}
```
`initialize` is called directly on `form()` method call.
`validate` will be called if the request's method is either `post`, `put`, `patch` or `delete`, which implies **adding** / **editing** / **deleting**.
`execute` will finally launch only if `validate` returns `true`. This is usually where you want to **save** / **destroy** your entities.
+13
View File
@@ -0,0 +1,13 @@
# `ui:checkbox` (string $name, mixed $value = null, array $attributes = [], array $options = [])
Built on top of `ui:radio` component, which itself extends `ui:input`.
The following attributes are assigned by this extension :
- `'class' => "ui-checkbox"`
- `'type' => "checkbox"`
and will typically be rendered such as :
```html
<input class="ui-checkbox" type="checkbox" value="$value" name="$name">
```
+13
View File
@@ -0,0 +1,13 @@
# `ui:color` (string $name, mixed $value = null, array $attributes = [], array $options = [])
Built on top of `ui:input`.
The following attributes are assigned by this extension :
- `'class' => "ui-color"`
- `'type' => "color"`
and will typically be rendered such as :
```html
<input class="ui-color" type="color" value="$value" name="$name">
```
+13
View File
@@ -0,0 +1,13 @@
# `ui:date` (string $name, mixed $value = null, array $attributes = [], array $options = [])
Built on top of `ui:input`.
The following attributes are assigned by this extension :
- `'class' => "ui-date"`
- `'type' => "date"`
and will typically be rendered such as :
```html
<input class="ui-date" type="date" value="$value" name="$name">
```
+13
View File
@@ -0,0 +1,13 @@
# `ui:datetime` (string $name, mixed $value = null, array $attributes = [], array $options = [])
Built on top of `ui:input`.
The following attributes are assigned by this extension :
- `'class' => "ui-datetime"`
- `'type' => "datetime-local"`
and will typically be rendered such as :
```html
<input class="ui-datetime" type="datetime-local" value="$value" name="$name">
```
+13
View File
@@ -0,0 +1,13 @@
# `ui:email` (string $name, mixed $value = null, array $attributes = [], array $options = [])
Built on top of `ui:input`.
The following attributes are assigned by this extension :
- `'class' => "ui-email"`
- `'type' => "email"`
and will typically be rendered such as :
```html
<input class="ui-email" type="email" value="" name="">
```
+13
View File
@@ -0,0 +1,13 @@
# `ui:file` (string $name, mixed $value = null, array $attributes = [], array $options = [])
Built on top of `ui:input`.
The following attributes are assigned by this extension :
- `'class' => "ui-file"`
- `'type' => "file"`
and will typically be rendered such as :
```html
<input class="ui-file" type="file" value="" name="">
```
+34
View File
@@ -0,0 +1,34 @@
# `ui:form` / `ui:form.get` / `ui:form.post` (string $method = "get", string $name = "", string $action = "", array $attributes = [])
A version exists also for unsupported methods (as of HTML5) on forms `ui:form.put` / `ui:form.patch` / `ui:form.delete`, which
could find a use within a Javascript environment.
The following attributes are assigned by this extension :
- `'class' => "ui-form"`
- `'enctype' => "multipart/form-data"`
- `'method' => "get|post|put|patch|delete"`
and will typically be rendered such as :
```html
<form class="ui-form" enctype="multipart/form-data" action="$action" method="$method">
<input class="ui-hidden" type="hidden" name="picea-ui-form[$name]" value="{ random md5 hash }">
</form>
```
### Additional options
You can now remove the enctype attribute `no-enctype` or the CSRF token `no-csrf` using those option
on the form tag :
```html
{% ui:form.post.no-enctype.no-csrf "my.form" %} {% endform %}
```
and would render as such :
```html
<form class="ui-form" action="$action" method="$method">
</form>
```
+13
View File
@@ -0,0 +1,13 @@
# `ui:hidden` (string $name, mixed $value = null, array $attributes = [], array $options = [])
Built on top of `ui:input`.
The following attributes are assigned by this extension :
- `'class' => "ui-hidden"`
- `'type' => "hidden"`
and will typically be rendered such as :
```html
<input class="ui-hidden" type="hidden" value="" name="">
```
+12
View File
@@ -0,0 +1,12 @@
# `ui:image` (string $source, array $attributes = [], array $options = [])
Built on top of `ui:input`.
The following attributes are assigned by this extension :
- `'class' => "ui-image"`
and will typically be rendered such as :
```html
<img class="ui-image" src="$source">
```
+13
View File
@@ -0,0 +1,13 @@
# `ui:input` (string $name, mixed $value = null, array $attributes = [], array $options = [])
This is small
The following attributes are assigned by this extension :
- `'class' => "ui-input"`
and will typically be rendered such as :
```html
<input class="ui-input" type="input" value="$value" name="$name">
```
+13
View File
@@ -0,0 +1,13 @@
# `ui:numeric` (string $name, mixed $value = null, array $attributes = [], array $options = [])
Built on top of `ui:input`.
The following attributes are assigned by this extension :
- `'class' => "ui-numeric"`
- `'type' => "numeric"`
and will typically be rendered such as :
```html
<input class="ui-numeric" type="numeric" value="$value" name="$name">
```
+13
View File
@@ -0,0 +1,13 @@
# `ui:password` (string $name, mixed $value = null, array $attributes = [], array $options = [])
Built on top of `ui:input`.
The following attributes are assigned by this extension :
- `'class' => "ui-password"`
- `'type' => "password"`
and will typically be rendered such as :
```html
<input class="ui-password" type="password" value="$value" name="$name">
```
+13
View File
@@ -0,0 +1,13 @@
# `ui:radio` (string $name, mixed $value = null, array $attributes = [], array $options = [])
Built on top of `ui:input`.
The following attributes are assigned by this extension :
- `'class' => "ui-radio"`
- `'type' => "radio"`
and will typically be rendered such as :
```html
<input class="ui-radio" type="radio" value="$value" name="$name">
```
+13
View File
@@ -0,0 +1,13 @@
# `ui:range` (string $name, mixed $value = null, array $attributes = [], array $options = [])
Built on top of `ui:input`.
The following attributes are assigned by this extension :
- `'class' => "ui-range"`
- `'type' => "range"`
and will typically be rendered such as :
```html
<input class="ui-range" type="range" value="$value" name="$name">
```
+13
View File
@@ -0,0 +1,13 @@
# `ui:search` (string $name, mixed $value = null, array $attributes = [], array $options = [])
Built on top of `ui:input`.
The following attributes are assigned by this extension :
- `'class' => "ui-search"`
- `'type' => "search"`
and will typically be rendered such as :
```html
<input class="ui-search" type="search" value="$value" name="$name">
```
+13
View File
@@ -0,0 +1,13 @@
# `ui:select` (string $name, array $list, $value = null, array $attributes = [], bool $strictComparison = true)
Built on top of `ui:input`.
The following attributes are assigned by this extension :
- `'class' => "ui-select"`
- `'type' => "select"`
and will typically be rendered such as :
```html
<input class="ui-select" type="select" value="$value" name="$name">
```
+13
View File
@@ -0,0 +1,13 @@
# `ui:tel` (string $name, mixed $value = null, array $attributes = [], array $options = [])
Built on top of `ui:input`.
The following attributes are assigned by this extension :
- `'class' => "ui-tel"`
- `'type' => "tel"`
and will typically be rendered such as :
```html
<input class="ui-tel" type="tel" value="$value" name="$name">
```
+13
View File
@@ -0,0 +1,13 @@
# `ui:text` (string $name, mixed $value = null, array $attributes = [], array $options = [])
Built on top of `ui:input`.
The following attributes are assigned by this extension :
- `'class' => "ui-text"`
- `'type' => "text"`
and will typically be rendered such as :
```html
<input class="ui-text" type="text" value="$value" name="$name">
```
+12
View File
@@ -0,0 +1,12 @@
# `ui:textarea` / `ui:textarea.raw` (string $name, mixed $value = null, array $attributes = [], array $options = [])
Built on top of `ui:input`. This extension also comes with a raw modifier which skips the escaping.
The following attributes are assigned by this extension :
- `'class' => "ui-textarea"`
and will typically be rendered such as :
```html
<textarea class="ui-textarea" name="$name">$value</textarea>
```
+13
View File
@@ -0,0 +1,13 @@
# `ui:time` (string $name, mixed $value = null, array $attributes = [], array $options = [])
Built on top of `ui:input`.
The following attributes are assigned by this extension :
- `'class' => "ui-time"`
- `'type' => "time"`
and will typically be rendered such as :
```html
<input class="ui-time" type="time" value="$value" name="$name">
```
+13
View File
@@ -0,0 +1,13 @@
# `ui:url` (string $name, mixed $value = null, array $attributes = [], array $options = [])
Built on top of `ui:input`.
The following attributes are assigned by this extension :
- `'class' => "ui-url"`
- `'type' => "url"`
and will typically be rendered such as :
```html
<input class="ui-url" type="url" value="$value" name="$name">
```
+13
View File
@@ -0,0 +1,13 @@
# `ui:week` (string $name, mixed $value = null, array $attributes = [], array $options = [])
Built on top of `ui:input`.
The following attributes are assigned by this extension :
- `'class' => "ui-week"`
- `'type' => "week"`
and will typically be rendered such as :
```html
<input class="ui-week" type="week" value="$value" name="$name">
```
+107
View File
@@ -0,0 +1,107 @@
# `form`(FormInterface $form, ? FormContext $formContext = null) : FormHandler
The `form()` method will process a form in three steps :
1. `initialize(FormContextInterface $context)` is first called on form processing.
2. `validate(FormContextInterface $context) : bool` will execute on POST. If you define a name to the form, it will match the one found in the POST (`picea-ui-form[$context->name]`) ; if the form is not the target, it will be skipped. Also, if an error si found within the message list, the method will return `false`.
3. `execute(FormContextInterface $context)` is the last method to run, which is usually where you save your entity and finilize your messages. The return value is accessible from the `formHandler` which `form()` returns.
**[PHP]** So, using this code:
in */app/controller/user.php*
```php
form(new \MyApp\Form\User\Create());
```
*/app/form/create.php*
```php
<?php
namespace MyProject\Form;
use MyProject\{ Lib, Entity };
use Picea\Ui\Method\{ FormContextInterface, Message\ErrorMessage };
use Psr\Http\Message\ServerRequestInterface;
use function MyProject\View\{ lang };
class Create implements \Picea\Ui\Method\FormInterface {
public function initialize(FormContextInterface $context) : void
{
$this->user = new Entity\User();
# $context->{varname} will be populated by $_POST data.
$this->user = (string) $context->fullname;
}
public function validate(FormContextInterface $context) : bool
{
if ( strlen($this->user) < 3 ) {
# This won't allow `execute` to run because of the error message.
$context->pushMessage(Lib\Message::generateError(
lang("form.error.provide_fullName")
));
}
return $context->valid();
}
public function execute(FormContextInterface $context) : void
{
# Saving a new user here
Entity\User::repository()->save($this->user);
$context->pushMessage(Lib\Message::generateSuccess(
lang("form.save")
));
}
}
```
which implements this interface :
```php
interface FormInterface
{
public function initialize(FormContextInterface $context) : void;
public function validate(FormContextInterface $context) : bool;
public function execute(FormContextInterface $context);
}
```
Would yield the same result such as:
```php
true
true
```
Trying to access either variable before `execute` is called will return `null`.
## `FormContext` object
`__construct(ServerRequestInterface $request, ? string $formName = null)`
Creating a form context manually allows to define the formName, which is essential if you want to use the
Cross-Scripting Request Forgery (CSRF).
It is populated with either $_POST or request's body which is json_decode. A property called `files` is also populated from PSR's `$request->getUploadedFiles()`.
It's `valid() : bool` method called into a form `validate()` function is first checking every message which were pushed into, using `pushMessage(FormMessage $message)`, and returns false if an error message is found in the list.
### Retrieve a form execution status / value
There is two ways to get the execution status (from the `execute` return value):
```php
$formContext = new \Picea\Ui\Method\FormContext($request, 'form.user_create');
$form = form(new \MyProject\Form\User\Create(), $formContext);
if ($formContext->formExecuted) {
echo $form->executionStatus;
echo $formContext->formExecutionStatus;
}
```
+8
View File
@@ -0,0 +1,8 @@
# `ui:popup` (string $name, array $variables = [], array $options = [])
The following attributes are assigned by this extension :
and will typically be rendered such as (directly inside an opening tag) :
```html
ui-popup='{ "name":$name,"vars":{$variables},"options":{$options} }'
```
+29
View File
@@ -0,0 +1,29 @@
<?php
use function DI\autowire, DI\create, DI\get;
use Picea\{ Method\Request };
use Picea\Extension\{ LanguageExtension, TitleExtension, NumberExtension, UrlExtension, DiExtension };
use Picea\Ui\{ Method, Ui };
return [
Ui::class => autowire(Ui::class),
'picea.extensions' => function(\Psr\Container\ContainerInterface $c) {
return array_merge([
$c->get(LanguageExtension::class),
$c->get(TitleExtension::class),
$c->get(NumberExtension::class),
$c->get(UrlExtension::class),
$c->get(Method\Form::class),
$c->get(Method\Pagination::class),
$c->get(Request::class),
$c->get(DiExtension::class),
], class_exists(\Taxus\Picea\Extension::class) ? [ $c->get(\Taxus\Picea\Extension::class) ] : [],
array_map(fn($class) => $c->get($class), $c->get(Lean\Lean::class)->getPiceaExtensions())
);
},
];
+67 -117
View File
@@ -2,20 +2,35 @@
namespace Picea\Ui\Common;
class UiElement implements \ArrayAccess, \Iterator, \JsonSerializable {
enum UiElementInsertMode
{
case append;
case prepend;
}
static array $config = [];
const INSERT_MODE_APPEND = 1;
const INSERT_MODE_PREPEND = 2;
public string $tag = 'div';
public array $attributes = [
'style' => [],
'class' => [],
class UiElement implements \JsonSerializable {
public const TAG_CONFIG_OPTIONS = [
"!doctype" => [ "attributes" => [ "html" ], "options" => [ "tag-type" => "single" ] ],
"js" => [ "tag" => "script", "attributes" => [ "type" => "text/javascript" ] ],
"css" => [ "tag" => "link", "attributes" => [ "rel" => "stylesheet" , "type" => "text/css" ] ],
"document" => [ "options" => [ "no-tag" => true ] ],
"link" => [ "options" => [ "tag-type" => "single" ] ],
"meta" => [ "options" => [ "tag-type" => "single" ] ],
"img" => [ "options" => [ "tag-type" => "single" ] ],
"input" => [ "options" => [ "tag-type" => "single" ] ],
"br" => [ "options" => [ "tag-type" => "single" ] ],
"hr" => [ "options" => [ "tag-type" => "single" ] ],
"iframe" => [ "options" => [ "tag-type" => "single" ] ],
"area" => [ "options" => [ "tag-type" => "single" ] ],
"col" => [ "options" => [ "tag-type" => "single" ] ],
"frame" => [ "options" => [ "tag-type" => "single" ] ],
"param" => [ "options" => [ "tag-type" => "single" ] ],
"video" => [ "options" => [ "tag-type" => "single" ] ],
"wbr" => [ "options" => [ "tag-type" => "single" ] ]
];
public array $attributes = [ 'style' => [], 'class' => [], ];
public array $childs = [];
/**
@@ -26,35 +41,20 @@ class UiElement implements \ArrayAccess, \Iterator, \JsonSerializable {
* tag-type single Only render a single tag. Cannot contains any childrens
* force-tag-open tag Control the way the opening tag is rendered
* force-tag-close tag-close " " " " closing " " "
* no-attr Attributes will not be rendered
* no-id ID is not automatically given (* may be removed *)
* escape-tag-end If you need to echo the Node inside another string, it may be useful to escape the "/" char using "\/"
* no-attr true Attributes will not be rendered
* no-id true ID is not automatically given (* may be removed *)
* escape-tag-end true If you need to echo the Node inside another string, it may be useful to escape the "/" char using "\/"
*/
public array $options = [];
public $selected = null;
public string $content = "";
protected ?UiQuery $kwery = null;
public function __construct(
public string $tag = "div",
) {}
public function __construct(? string $tag = null) {
if ( ! static::$config ) {
static::pushConfigArray( include(dirname(__FILE__) . "/taglist.php") );
}
if ($tag !== null) {
$this->tag = $tag;
}
}
public static function pushConfigArray(array $array) : void
{
static::$config = array_replace_recursive(static::$config, $array);
}
public static function createStylesheet(string $href, $attributes = [], $options = []) : self
public static function stylesheet(string $href, $attributes = [], $options = []) : self
{
return static::create('link', $attributes + [
'href' => $href,
@@ -63,7 +63,7 @@ class UiElement implements \ArrayAccess, \Iterator, \JsonSerializable {
], [ 'tag-type' => 'single' ] + $options);
}
public static function createScript(string $src, array $attributes = [], array $options = []) : self
public static function script(string $src, array $attributes = [], array $options = []) : self
{
return static::create('script', $attributes + [
'src' => $src,
@@ -71,7 +71,7 @@ class UiElement implements \ArrayAccess, \Iterator, \JsonSerializable {
], $options);
}
public static function createPhp(array $attributes = [], array $options = []) : self
public static function php(array $attributes = [], array $options = []) : self
{
return static::create('', $attributes, [
'force-tag-open' => '<?php ',
@@ -108,7 +108,7 @@ class UiElement implements \ArrayAccess, \Iterator, \JsonSerializable {
$obj->attributes($attributes);
}
if ( false !== ( $custom = static::$config["tags"][$tag] ?? false ) ) {
if ( false !== ( $custom = static::TAG_CONFIG_OPTIONS[strtolower($tag)] ?? false ) ) {
if ( $custom['tag'] ?? false ) {
$obj->tag = $custom['tag'];
}
@@ -148,7 +148,7 @@ class UiElement implements \ArrayAccess, \Iterator, \JsonSerializable {
return $this;
}
public function option(string $var, $value = 1) : self
public function option(string $var, mixed $value = true) : self
{
$this->options[$var] = $value;
@@ -177,8 +177,10 @@ class UiElement implements \ArrayAccess, \Iterator, \JsonSerializable {
}
}
else if ( !is_numeric($key) ) {
# will output something like <tag $key=$value></tag>
$attributesList[] = strpos($value, '"') !== false ? "$key='$value'" : "$key=\"$value\"";
$value = htmlspecialchars($value);
# will output something like <tag $key="$value"></tag>
$attributesList[] = "$key=\"$value\"";
}
else {
# will output something like <tag $value></tag>
@@ -198,14 +200,14 @@ class UiElement implements \ArrayAccess, \Iterator, \JsonSerializable {
}
}
if ( in_array('no-tag', $this->options, true) ) {
if ( $this->options['no-tag'] ?? false ) {
return $this->content . $content;
}
else {
$compiledAttributes = (count($attributesList) && ! in_array('no-attr', $this->options, true) ? " " . implode(" ", $attributesList) : "");
$compiledAttributes = $this->options['no-attr'] ?? false ? "" : " " . implode(" ", $attributesList);
# Force the node to contain a certain opening tag (php is a good example)
if ( in_array('force-tag-open', $this->options, true) ) {
if ( $this->options['force-tag-open'] ?? false ) {
$opentag = $this->options['force-tag-open'] . $compiledAttributes;
}
else {
@@ -217,11 +219,11 @@ class UiElement implements \ArrayAccess, \Iterator, \JsonSerializable {
$closetag = "";
}
}
elseif ( in_array('force-tag-close', $this->options, true) ) {
elseif ( $this->options['force-tag-close'] ?? false ) {
$closetag = $this->options['force-tag-close'];
}
else {
$closetag = $this->tag ? "<" . (in_array('escape-tag-end', $this->options) ? "\/" : "/" ) . "{$this->tag}>" : "";
$closetag = $this->tag ? "<" . ( $this->options['escape-tag-end'] ?? false ? "\/" : "/" ) . "{$this->tag}>" : "";
}
return $opentag . $this->content . $content . $closetag;
@@ -240,7 +242,7 @@ class UiElement implements \ArrayAccess, \Iterator, \JsonSerializable {
public function text(?string $set = null) {
if ($set !== null) {
$this->content = htmlspecialchars( $set, ENT_NOQUOTES );
$this->content = htmlspecialchars( $set, \ENT_NOQUOTES );
return $this;
}
@@ -250,24 +252,23 @@ class UiElement implements \ArrayAccess, \Iterator, \JsonSerializable {
public function append( ...$arguments ) : self
{
return $this->insert( static::INSERT_MODE_APPEND, ...$arguments);
return $this->insert( UiElementInsertMode::append, ...$arguments);
}
public function prepend( ...$arguments ) : self
{
return $this->insert( static::INSERT_MODE_PREPEND, ...( is_array($arguments) ? array_reverse($arguments) : $arguments) );
return $this->insert( UiElementInsertMode::prepend, ...( is_array($arguments) ? array_reverse($arguments) : $arguments) );
}
protected function insert(int $mode, ...$elements) : self
protected function insert(UiElementInsertMode $mode, ...$elements) : self
{
foreach($elements as $item) {
switch($mode) {
case static::INSERT_MODE_APPEND:
case UiElementInsertMode::append:
array_push($this->childs, $item);
break;
case static::INSERT_MODE_PREPEND:
case UiElementInsertMode::prepend:
array_unshift($this->childs, $item);
break;
}
@@ -285,14 +286,24 @@ class UiElement implements \ArrayAccess, \Iterator, \JsonSerializable {
break;
case 'style' :
foreach($value as $var => $val) {
$this->css($var, $val);
if ( is_string($value) )
{
foreach(array_filter(explode(";", $value)) as $vars) {
list($key, $value) = explode(':', $vars);
$this->css($key, $value);
}
}
elseif ( is_array($value) ) {
foreach($value as $var => $val) {
$this->css($var, $val);
}
}
break;
default:
if ( is_numeric($key) ) {
$this->attributes[] = $value;
$this->attributes[$key] = $value;
}
elseif ( is_array($this->attributes[$key] ?? false) ) {
$this->attributes[$key] = array_replace($value, $this->attributes[$key]);
@@ -316,7 +327,6 @@ class UiElement implements \ArrayAccess, \Iterator, \JsonSerializable {
*/
public function css(...$arguments) {
foreach($arguments as $item) {
if ( is_array($item) ) {
foreach($item as $key => $value) {
$this->css($key, $value);
@@ -335,15 +345,12 @@ class UiElement implements \ArrayAccess, \Iterator, \JsonSerializable {
return array_search(strtolower($className), array_map('strtolower', $this->attributes['class']), true);
}
public function delete() {
}
public function count() {
return count($this->childs);
}
public function jsonSerialize() {
public function jsonSerialize() : mixed
{
return [
'tag' => $this->tag,
'attr' => $this->attributes,
@@ -352,63 +359,6 @@ class UiElement implements \ArrayAccess, \Iterator, \JsonSerializable {
];
}
public function offsetSet($offset, $value) {
if ( is_numeric($offset) ) {
return $this->selected[$offset] = $value;
}
elseif ( is_null($offset) ) {
return $this->childs[] = $value;
}
else {
return $this->childs[$offset] = $value;
}
}
public function offsetExists($query) : bool
{
return count($this->kwery()->find($query)) > 0;
}
public function offsetUnset($query) : void
{
$this->kwery()->find($query)->remove();
}
public function offsetGet($query) : self
{
return $this->kwery()->find($query);
}
public function rewind() : void
{
reset($this->childs);
}
public function current() : self
{
return current($this->childs);
}
public function key() : int
{
return key($this->childs);
}
public function next() : self
{
return next($this->childs);
}
public function valid() : bool
{
return ! in_array(key($this->childs), [ NULL, FALSE ], true);
}
public static function isNode($obj) : bool
{
return $obj instanceof UiElement;
}
public function __toString() : string
{
return $this->render();
+2 -2
View File
@@ -9,7 +9,7 @@ use Picea\Extension\ExtensionTrait;
class UiMessage extends UiElement implements Extension {
use ExtensionTrait;
public string $token = "ui.message";
public string $token = "ui:message";
public string $tag = "input";
@@ -17,7 +17,7 @@ class UiMessage extends UiElement implements Extension {
'class' => 'ui-message',
];
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) : string
public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token, array $options = []) : string
{
return "";
}
+10 -7
View File
@@ -9,22 +9,25 @@ use Picea\Extension\ExtensionTrait;
class UiPopup extends UiElement implements Extension {
use ExtensionTrait;
public string $token = "ui.popup";
public string $tag = "div";
public string $token = "ui:popup";
public array $attributes = [
'class' => 'ui-popup',
'class' => 'ui-popup',
];
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) : string
public function __construct(
public string $tag = "div",
) {}
public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token, array $options = []) : string
{
return "<?php echo 'ui-popup=\"' . ( new \\" . static::class . "() )->buildAttributes($arguments) . '\"' ?>";
}
public function buildAttributes(string $name, array $variables = []) : string
public function buildAttributes(string $name, array $variables = [], array $options = []) : string
{
return htmlentities(json_encode([ "name" => $name, "vars" => $variables ], JSON_HEX_APOS | JSON_HEX_QUOT), ENT_QUOTES, 'UTF-8');
return htmlentities(json_encode([ "name" => $name, "vars" => $variables, "options" => $options ], JSON_HEX_APOS | JSON_HEX_QUOT), \ENT_QUOTES, 'UTF-8');
}
}
+128
View File
@@ -0,0 +1,128 @@
<?php
namespace Picea\Ui\Form;
use Picea\Extension\Extension;
use Picea\Extension\ExtensionTrait;
use Picea\Ui\Common\UiElement;
use Picea\Ui\Component\UiMessage;
use Picea\Ui\Component\UiPopup;
class Ui extends UiElement implements Extension {
use ExtensionTrait;
public string $token = "ui";
protected /* ? mixed */ $value;
protected string $name;
public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token, array $options = []) : string
{
switch(true) {
case in_array('checkbox', $options):
$input = new UiCheckbox();
break;
case in_array('color', $options):
$input = new UiColor();
break;
case in_array('date', $options):
$input = new UiDate();
break;
case in_array('datetime', $options):
$input = new UiDatetime();
break;
case in_array('email', $options):
$input = new UiEmail();
break;
case in_array('file', $options):
$input = new UiFile();
break;
case in_array('form', $options):
case in_array('endform', $options):
$input = new UiForm();
break;
case in_array('hidden', $options):
$input = new UiHidden();
break;
case in_array('img', $options):
case in_array('image', $options):
$input = new UiImage();
break;
case in_array('input', $options):
$input = new UiInput();
break;
case in_array('numeric', $options):
$input = new UiNumeric();
break;
case in_array('password', $options):
$input = new UiPassword();
break;
case in_array('radio', $options):
$input = new UiRadio();
break;
case in_array('range', $options):
$input = new UiRange();
break;
case in_array('search', $options):
$input = new UiSearch();
break;
case in_array('select', $options):
$input = new UiSelect();
break;
case in_array('tel', $options):
$input = new UiTel();
break;
case in_array('text', $options):
$input = new UiText();
break;
case in_array('textarea', $options):
$input = new UiTextarea();
break;
case in_array('time', $options):
$input = new UiTime();
break;
case in_array('url', $options):
$input = new UiUrl();
break;
case in_array('week', $options):
$input = new UiWeek();
break;
case in_array('popup', $options):
$input = new UiPopup();
break;
case in_array('message', $options):
$input = new UiMessage();
break;
}
if (empty($input)) {
throw new \Exception("Missing token ? $token $arguments");
}
return $input->parse($context, $arguments, $token, $options);
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ namespace Picea\Ui\Form;
class UiCheckbox extends UiRadio {
public string $token = "ui.checkbox";
public string $token = "ui:checkbox";
public array $attributes = [
'class' => "ui-checkbox",
+1 -1
View File
@@ -4,7 +4,7 @@ namespace Picea\Ui\Form;
class UiColor extends UiInput {
public string $token = "ui.color";
public string $token = "ui:color";
public array $attributes = [
'class' => "ui-color",
+1 -1
View File
@@ -4,7 +4,7 @@ namespace Picea\Ui\Form;
class UiDate extends UiInput {
public string $token = "ui.date";
public string $token = "ui:date";
public array $attributes = [
'class' => "ui-date",
+1 -1
View File
@@ -4,7 +4,7 @@ namespace Picea\Ui\Form;
class UiDatetime extends UiInput {
public string $token = "ui.datetime";
public string $token = "ui:datetime";
public array $attributes = [
'class' => "ui-datetime",
+1 -1
View File
@@ -4,7 +4,7 @@ namespace Picea\Ui\Form;
class UiEmail extends UiInput {
public string $token = "ui.email";
public string $token = "ui:email";
public array $attributes = [
'class' => "ui-email",
+5 -5
View File
@@ -4,10 +4,10 @@ namespace Picea\Ui\Form;
class UiFile extends UiInput {
public string $token = "ui.file";
public string $token = "ui:file";
public array $attributes = [
'class' => "ui-file",
'type' => "file",
];
public array $attributes = [
'class' => "ui-file",
'type' => "file",
];
}
+61 -28
View File
@@ -11,64 +11,97 @@ class UiForm extends UiElement implements Extension {
public string $defaultMethod = "get";
public array $token = [ "ui.form", "ui.endform", "ui.form.get", "ui.form.post", "ui.form.patch", "ui.form.delete", "ui.form.put" ];
public string $tag = "form";
public array $token = [ "ui:form", "ui:endform", "ui:/form" ];
public array $attributes = [
'class' => 'ui-form',
];
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) : string
public function __construct(
public string $tag = "form",
public bool $enctype = true,
public bool $csrf = true,
) {}
public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token, array $options = []) : string
{
$constructor = [];
switch($token) {
case 'ui.endform':
case 'ui.endform': # bw compat
case 'ui:endform':
case 'ui:/form':
return "</form>";
}
case "ui.form.get":
$method = "get";
break;
if (in_array('no-enctype', $options)) {
$constructor[] = "enctype: false";
}
case "ui.form.patch":
$method = "patch";
break;
if (in_array('no-csrf', $options)) {
$constructor[] = "csrf: false";
}
case "ui.form.delete":
$method = "delete";
break;
case "ui.form.put":
$method = "put";
break;
case "ui.form.post":
$method = "post";
break;
if (in_array('get', $options)) {
$method = "get";
}
elseif (in_array('post', $options)) {
$method = "post";
}
elseif (in_array('put', $options)) {
$method = "put";
}
elseif (in_array('delete', $options)) {
$method = "delete";
}
elseif (in_array('patch', $options)) {
$method = "patch";
}
$method ??= $this->defaultMethod;
return "<?php echo ( new \\" . static::class . "() )->buildHtml('$method', $arguments) ?>";
$opt = var_export($options, true);
$constructor = implode(',', $constructor);
return "<?php echo ( new \\" . static::class . "($constructor) )->buildHtml('$method', $arguments) ?>";
}
public function buildHtml(string $method = "get", string $name = "", string $action = "", array $attributes = []) : string
{
$method = strtolower($method);
# Method passed in arguments take precedents over options
$method = strtolower($attributes['method'] ?? $method);
$this->option('tag-type', 'single');
if ($attributes['class'] ?? false) {
$attributes['class'] .= " {$this->attributes['class']}";
unset($this->attributes['class']);
}
$this->attributes([ 'method' => $method, 'action' => $action ] + $attributes);
$this->attributes([ 'action' => $action, 'method' => $method, ] + $attributes);
if ( $method !== "get" ) {
$this->append( ( new UiHidden() )->attributes([ 'name' =>"picea-ui-form[$name]", 'value' => md5( $name . microtime() ) ]) );
$this->attributes([ 'enctype' => "multipart/form-data" ]);
if ($this->csrf) {
$token = md5($name . microtime());
$key = "picea-ui.form:{$name}";
if (count($_SESSION[$key] ?? []) > 100) {
array_shift($_SESSION[$key]);
}
$_SESSION[$key][] = $token;
$this->append((new UiHidden())->attributes([
'name' => "picea-ui-form[$name]",
'value' => $token,
]));
}
if ($this->enctype) {
$this->attributes([ 'enctype' => "multipart/form-data" ]);
}
}
return $this->render() . PHP_EOL;
+1 -1
View File
@@ -4,7 +4,7 @@ namespace Picea\Ui\Form;
class UiHidden extends UiInput {
public string $token = "ui.hidden";
public string $token = "ui:hidden";
public array $attributes = [
'class' => "ui-hidden",
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace Picea\Ui\Form;
use Picea\Ui\Common\UiElement;
use Picea\Extension\Extension;
use Picea\Extension\ExtensionTrait;
class UiImage extends UiElement implements Extension {
use ExtensionTrait;
public array $tokens = [ "ui:img", "ui:image" ];
public array $attributes = [
'class' => 'ui-image',
];
public array $options = [
'tag-type' => "single",
];
public function __construct(
public string $tag = "img",
) {}
public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token, array $options = []) : string
{
return "<?php echo ( new \\" . static::class . "() )->buildHtml($arguments) ?>";
}
public function buildHtml(string $source, array $attributes = [], array $options = []) : string
{
if ($options) {
$this->options += $options;
}
if ($attributes['class'] ?? false) {
$attributes['class'] .= " {$this->attributes['class']}";
unset($this->attributes['class']);
}
$this->attributes([ 'src' => $source ] + $attributes + $this->attributes + $this->objectAttribute());
return $this->render();
}
protected function objectAttribute() : array
{
return [];
}
}
+24 -10
View File
@@ -3,15 +3,12 @@
namespace Picea\Ui\Form;
use Picea\Ui\Common\UiElement;
use Picea\Extension\Extension;
use Picea\Extension\ExtensionTrait;
use Picea\Extension\{ Extension, FunctionExtension, ExtensionTrait };
class UiInput extends UiElement implements Extension {
class UiInput extends UiElement implements Extension, FunctionExtension {
use ExtensionTrait;
public string $token = "ui.input";
public string $tag = "input";
public string $token = "ui:input";
public array $attributes = [
'class' => 'ui-input',
@@ -21,16 +18,33 @@ class UiInput extends UiElement implements Extension {
'tag-type' => "single",
];
protected /* ? mixed */ $value;
protected mixed $value;
protected string $name;
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) : string
public function __construct(
public string $tag = "input",
) {}
public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token, array $options = []) : string
{
return "<?php echo ( new \\" . static::class . "() )->buildHtml($arguments) ?>";
}
public function buildHtml(string $name, /*? mixed */ $value = null, array $attributes = [], array $options = []) : string
public function exportFunctions(): array
{
if ( null === $type = $this->attributes['type'] ?? null ) {
return [];
}
$key = str_replace('-', '_', 'ui_'. ( $this->attributes['type'] ?? 'text' ));
return [
$key => fn(string $name, mixed $value = null, array $attributes = [], array $options = []) : string => $this->buildHtml($name, $value, $attributes, $options),
];
}
public function buildHtml(string $name, mixed $value = null, array $attributes = [], array $options = []) : string
{
$this->name = $name;
@@ -43,7 +57,7 @@ class UiInput extends UiElement implements Extension {
}
if ($attributes['class'] ?? false) {
$attributes['class'] .= " {$this->attributes['class']}";
$attributes['class'] = implode(" ", array_merge((array) $attributes['class'], (array) $this->attributes['class']));
unset($this->attributes['class']);
}
+2 -2
View File
@@ -4,10 +4,10 @@ namespace Picea\Ui\Form;
class UiNumeric extends UiInput {
public string $token = "ui.numeric";
public string $token = "ui:numeric";
public array $attributes = [
'class' => "ui-numeric",
'type' => "numeric",
'type' => "number",
];
}
+1 -1
View File
@@ -4,7 +4,7 @@ namespace Picea\Ui\Form;
class UiPassword extends UiInput {
public string $token = "ui.password";
public string $token = "ui:password";
public array $attributes = [
'class' => "ui-password",
+2 -2
View File
@@ -4,7 +4,7 @@ namespace Picea\Ui\Form;
class UiRadio extends UiInput {
public string $token = "ui.radio";
public string $token = "ui:radio";
public array $attributes = [
'class' => "ui-radio",
@@ -13,7 +13,7 @@ class UiRadio extends UiInput {
protected function objectAttribute() : array
{
if ( ( $this->options['value'] ?? false ) && ( $this->options['value'] === $this->value ) ) {
if ( isset($this->options['value']) && ( $this->options['value'] === $this->value ) ) {
return [
'checked' => "checked"
];
+1 -1
View File
@@ -4,7 +4,7 @@ namespace Picea\Ui\Form;
class UiRange extends UiInput {
public string $token = "ui.range";
public string $token = "ui:range";
public array $attributes = [
'class' => "ui-range",
+1 -1
View File
@@ -4,7 +4,7 @@ namespace Picea\Ui\Form;
class UiSearch extends UiInput {
public string $token = "ui.search";
public string $token = "ui:search";
public array $attributes = [
'class' => "ui-search",
+30 -9
View File
@@ -9,21 +9,28 @@ use Picea\Extension\ExtensionTrait;
class UiSelect extends UiElement implements Extension {
use ExtensionTrait;
public string $token = "ui.select";
public string $tag = "select";
public string $token = "ui:select";
public array $attributes = [
'class' => 'ui-select',
];
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) : string
public function __construct(
public string $tag = "select",
) {}
public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token, array $options = []) : string
{
return "<?php echo ( new \\" . static::class . "() )->buildHtml($arguments) ?>";
}
public function buildHtml(string $name, array $list, $value = null, array $attributes = [], bool $strictComparison = true) : string
public function buildHtml(string $name, array $list, mixed $value = null, array $attributes = [], bool $strictComparison = true) : string
{
if ($attributes['class'] ?? false) {
$attributes['class'] = implode(" ", array_merge((array) $attributes['class'], (array) $this->attributes['class']));
unset($this->attributes['class']);
}
$this->attributes([ 'name' => $name ] + $attributes + $this->attributes);
$this->setList($list, $value, $strictComparison);
@@ -38,7 +45,6 @@ class UiSelect extends UiElement implements Extension {
protected function setList(array $list, $selected, bool $strict = true) : void
{
foreach($list as $key => $item) {
if ($item instanceof UiElement) {
$this->append($item);
@@ -49,11 +55,11 @@ class UiSelect extends UiElement implements Extension {
$obj->attributes(['label' => $key]);
foreach($item as $subKey => $subItem) {
$obj->append($this->createOption($subItem, $subKey, $this->isSelected($subKey, $selected, $strict)));
$obj->append($this->createOption((string) $subItem, $subKey, $this->isSelected($subKey, $selected, $strict)));
}
}
else {
$obj = $this->createOption($item, $key, $this->isSelected($key, $selected, $strict));
$obj = $this->createOption((string) $item, $key, $this->isSelected($key, $selected, $strict));
}
$this->append($obj);
@@ -73,8 +79,23 @@ class UiSelect extends UiElement implements Extension {
return $option;
}
protected function isSelected($check, $value, bool $strict = true) : bool
protected function isSelected($check, mixed $value, bool $strict = true) : bool
{
if ( is_array($value) ) {
return in_array($check, array_map(fn($e) => $this->normalizeValue($e), $value), $strict);
}
$value = $this->normalizeValue($value);
return $strict ? $check === $value : $check == $value;
}
protected function normalizeValue(mixed $value) : mixed
{
if ($value instanceof \BackedEnum) {
return $value->value;
}
return $value;
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ namespace Picea\Ui\Form;
class UiTel extends UiInput {
public string $token = "ui.tel";
public string $token = "ui:tel";
public array $attributes = [
'class' => "ui-tel",
+1 -1
View File
@@ -4,7 +4,7 @@ namespace Picea\Ui\Form;
class UiText extends UiInput {
public string $token = "ui.text";
public string $token = "ui:text";
public array $attributes = [
'class' => "ui-text",
+22 -4
View File
@@ -4,9 +4,7 @@ namespace Picea\Ui\Form;
class UiTextarea extends UiInput {
public string $token = "ui.textarea";
public string $tag = "textarea";
public array $tokens = [ "ui:textarea" ];
public array $attributes = [
'class' => "ui-textarea",
@@ -14,8 +12,28 @@ class UiTextarea extends UiInput {
public array $options = [];
protected bool $echoRaw = false;
public function __construct(
public string $tag = "textarea",
public bool $raw = false,
) {}
public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token, array $options = []) : string
{
$raw = in_array('raw', $options) ? 'true' : 'false';
return "<?php echo ( new \\" . static::class . "(raw: $raw) )->buildHtml($arguments) ?>";
}
protected function setValue($value) : void
{
$this->html($value);
$this->echoRaw ? $this->html($value) : $this->text($value);
}
protected function echoRaw(bool $value) : self
{
$this->echoRaw = $value;
return $this;
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ namespace Picea\Ui\Form;
class UiTime extends UiInput {
public string $token = "ui.time";
public string $token = "ui:time";
public array $attributes = [
'class' => "ui-time",
+1 -1
View File
@@ -4,7 +4,7 @@ namespace Picea\Ui\Form;
class UiUrl extends UiInput {
public string $token = "ui.url";
public string $token = "ui:url";
public array $attributes = [
'class' => "ui-url",
+1 -1
View File
@@ -4,7 +4,7 @@ namespace Picea\Ui\Form;
class UiWeek extends UiInput {
public string $token = "ui.week";
public string $token = "ui:week";
public array $attributes = [
'class' => "ui-week",
@@ -0,0 +1,8 @@
<?php
namespace Picea\Ui\Method\Context;
interface FormContextCastableInterface
{
public static function instance(mixed $value);
}
+8 -22
View File
@@ -7,9 +7,10 @@ use Picea\Extension\Extension,
use Picea\Compiler\Context;
use Picea\Extension\FunctionExtension;
use Psr\Http\Message\ServerRequestInterface;
class Form implements Extension {
class Form implements Extension, FunctionExtension {
use ExtensionTrait;
public array $tokens;
@@ -22,34 +23,19 @@ class Form implements Extension {
public function __construct(ServerRequestInterface $request, Context $context) {
$this->request = $request;
$this->register($context);
}
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) : string { }
public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token, array $options = []) : string { }
public function register(Context $context) : void
public function exportFunctions(): array
{
$context->pushFunction("form", [ $this, 'formClass' ]);
}
public function form_csrf(string $field, string $value) {
$values = $this->session("View.form.csrf.$field") ?: [];
# keeps 20 (from config) latest CSRF key for this form into session,
# allowing more than one tab opened and preventing information loss
if ( count($values) >= 20 ) {
#array_shift($values);
}
$values[] = $value;
$this->session("View.form.csrf.$field", $values);
return $value;
return [
"form" => [ $this, 'formClass' ],
];
}
public function formClass(FormInterface $form, ? FormContext $formContext = null) : FormHandler
{
return new FormHandler($this->request, $form, $formContext);
return new FormHandler($formContext ? $formContext->getRequest() : $this->request, $form, $formContext);
}
}
+179 -22
View File
@@ -7,32 +7,67 @@ use Psr\Http\Message\ServerRequestInterface,
class FormContext implements FormContextInterface
{
public string $formName;
public string $formContextMethod;
public bool $formSent;
public bool $formSent = false;
public array $values = [];
public bool $formExecuted = false;
public array $messages = [];
public mixed $formExecutionStatus = null;
public ServerRequestInterface $request;
protected string $formContextValuesRoot;
public ? ResponseInterface $response = null;
public array $formContextValues = [];
public function __construct(ServerRequestInterface $request, ? string $formName = null)
{
$this->request = $request;
public array $formContextFiles = [];
if ( $formName ) {
$this->formName = $formName;
public array $formContextMessages = [];
public bool $formSkipCsrf = false;
protected array $formContextDefinedProperties = [];
protected array $formContextCatchedMethods = [ 'POST', 'PUT', 'PATCH', 'DELETE', ];
protected array $formConstructorProperties = [];
public ? ResponseInterface $formContextResponse = null;
public function __construct(
public ServerRequestInterface $formContextRequest,
public readonly ? string $formName = null
) {
# From constructor
$reflection = new \ReflectionClass($this);
$this->formConstructorProperties = array_map(fn($e) => strtolower($e->getName()), array_filter($reflection->getConstructor()->getParameters(), fn($e) => $e->isPromoted()));
$this->formContextValues = $formContextRequest->getParsedBody() ?: [];
if ( ! $this->formContextValues ) {
$content = mb_convert_encoding((string) $formContextRequest->getBody(), 'UTF-8');
if ( $content && ( $json = json_decode($content, true) ) ) {
$this->formContextValues = $json;
}
}
$this->values = $request->getParsedBody() ?: [];
if ($this->formContextValuesRoot ?? false) {
$this->formContextValues = $this->formContextValues[$this->formContextValuesRoot] ?? [];
}
$this->fillValues();
$this->formContextFiles = $formContextRequest->getUploadedFiles() ?: [];
$this->initialize();
}
public function initialize() : void {}
public function valid() : bool
{
foreach($this->messages as $message) {
foreach($this->formContextMessages as $message) {
if ( $message->isError() ) {
return false;
}
@@ -41,9 +76,47 @@ class FormContext implements FormContextInterface
return true;
}
public function executed() : bool
{
return $this->formExecutionStatus === false ? false : $this->formExecuted;
}
public function formSent() : bool
{
return $this->formSent;
$valid = in_array($this->requestMethod(), $this->formContextCatchedMethods);
if ( (! $this->formSkipCsrf) && ! empty($this->formName) ) {
$token = $this->get('picea-ui-form')[$this->formName] ?? false;
if ( $token ) {
if (! $this->formSkipCsrf) {
$valid = in_array($token, $_SESSION["picea-ui.form:{$this->formName}"] ?? []);
}
else {
$valid = (bool) $token;
}
}
else {
$valid = false;
}
}
return $this->formSent = $valid;
}
public function getExecutionStatus() : mixed
{
return $this->formExecutionStatus;
}
public function getRequest() : ServerRequestInterface
{
return $this->formContextRequest;
}
public function requestMethod() : string
{
return $this->formContextRequest->getMethod();
}
public function __set($key, $value)
@@ -58,7 +131,7 @@ class FormContext implements FormContextInterface
public function __isset($key)
{
return array_key_exists($key, $this->values);
return array_key_exists($key, $this->formContextValues);
}
public function __unset($key)
@@ -66,28 +139,112 @@ class FormContext implements FormContextInterface
$this->delete($key);
}
public function get(string $key, $default = null)
public function get(string $key, mixed $default = null)
{
return $this->has($key) ? $this->values[$key] : $default;
return $this->has($key) ? $this->formContextValues[$key] : $default;
}
public function set(string $key, $value)
public function set(string $key, mixed $value)
{
return $this->values[$key] = $value;
if ($this->canWriteProperty($key)) {
$this->$key = $this->castValue($key, $value);
}
return $this->formContextValues[$key] = $value;
}
public function sets(array $values) : static
{
foreach($values as $key =>$value) {
$this->set($key, $value);
}
return $this;
}
public function delete(string $key) : void
{
unset($this->values[$key]);
unset($this->formContextValues[$key]);
}
public function has(string $key) : bool
{
return array_key_exists($key, $this->values);
return array_key_exists($key, $this->formContextValues);
}
public function pushMessage(FormMessage $message) : void
{
$this->messages[] = $message;
if ($message->isError() /* && $message->isSuccess() @TODO next major v. */ ) {
$this->formContextMessages = array_merge([ $message ], $this->formContextMessages);
}
else {
$this->formContextMessages[] = $message;
}
}
public function messages() : array
{
return $this->formContextMessages;
}
protected function fillValues() : void
{
foreach($this->formContextValues as $property => $value) {
if ($this->canWriteProperty($property)) {
$this->$property = $this->castValue($property, $value);
$this->formContextDefinedProperties[$property] = true;
}
}
}
protected function canWriteProperty(string $property) : bool
{
# Skipping overrides of this particular class vars as a security measure
static $skipping = array_keys(array_change_key_case(get_class_vars(FormContext::class), CASE_LOWER));
return ! in_array(strtolower($property), array_merge($skipping, $this->formConstructorProperties)) && property_exists($this, $property);
}
protected function definedProperty(string $property) : bool
{
return $this->formContextDefinedProperties[$property] ?? false;
}
protected function castValue(string $property, mixed $value) : mixed
{
$cast = fn($e) => match(true) {
is_a($e, Context\FormContextCastableInterface::class, true) => $e::instance($value),
$e === "array" => (function(array|string $value) use ($property) {
if (is_string($value)) {
return json_validate($value) ? json_decode($value, true) ?? [] : throw new \InvalidArgumentException("Field '$property' is awaiting JSON encoded content.");
}
return $value;
})($value),
is_null($value) => null,
enum_exists($e) => ( $value instanceof \BackedEnum ? $value : $e::tryFrom($value) ) ?? throw new \InvalidArgumentException(sprintf("Field '\$$property' awaiting values : '%s'.", implode("', '", array_map(fn($e) => $e->value, $e::cases())))),
function_exists($e) => $e($value),
class_exists($e) => new $e(...$value),
interface_exists($e) => throw new \InvalidArgumentException("There is no way provided to match an interface object yet."),
default => $value
};
$types = ( new \ReflectionProperty($this, $property) )->getType();
if ($types instanceof \ReflectionNamedType) {
$value = $cast($types->getName());
}
elseif ($types instanceof \ReflectionIntersectionType || $types instanceof \ReflectionUnionType) {
foreach($types->getTypes() as $type) {
return $cast($type->getName());
}
}
elseif ($types->isBuiltin()) {
return $cast($types->getName());
}
return $value;
}
}
+8
View File
@@ -2,8 +2,16 @@
namespace Picea\Ui\Method;
use Psr\Http\Message\ServerRequestInterface;
interface FormContextInterface {
public function valid() : bool;
public function formSent() : bool;
public function requestMethod() : string;
public function getRequest() : ServerRequestInterface;
public function getExecutionStatus() : mixed;
}
+18 -19
View File
@@ -5,15 +5,25 @@ namespace Picea\Ui\Method;
use Psr\Http\Message\ServerRequestInterface;
class FormHandler {
const DEFAULT_METHODS = [
"DELETE", "PATCH", "POST", "PUT",
];
public bool $sent = false;
public ? bool $executionStatus = null;
public bool $validateCsrfToken = true;
public /* mixed */ $executionStatus = null;
public ? bool $validationStatus = null;
public FormInterface $form;
public FormContext $context;
protected ServerRequestInterface $request;
protected FormInterface $form;
public array $acceptedMethods = self::DEFAULT_METHODS;
public function __construct(ServerRequestInterface $request, FormInterface $form, ? FormContextInterface $context = null)
{
@@ -28,37 +38,26 @@ class FormHandler {
$this->context = method_exists($form, 'getContext') ? $form->getContext($request) : new FormContext($request);
}
$this->request->context = $this->context;
$this->request->withAttribute('picea.context', $this->context);
$this->formSent();
$this->initialize();
}
public function formSent() : void
{
if ( false !== $this->context->formSent = $this->sent ) {
if ( $this->context->formName ?? false ) {
$this->sent = $this->context->formSent = (bool) ( $this->request->getParsedBody()['picea-ui-form'][$this->context->formName] ?? false );
}
}
}
protected function initialize() : void
{
$this->form->initialize($this->context);
if ( $this->sent ) {
if ( $this->form->validate($this->context) ) {
$this->executionStatus = $this->form->execute($this->context);
if ( $this->sent && $this->context->formSent() ) {
if ( $this->validationStatus = $this->form->validate($this->context) ) {
$this->context->formExecutionStatus = $this->executionStatus = $this->form->execute($this->context);
$this->context->formExecuted = true;
}
}
}
protected function requestSent() : bool
{
return in_array($this->request->getMethod(), [
"DELETE", "PATCH", "POST", "PUT",
]);
return in_array(strtoupper($this->request->getMethod()), array_map('strtoupper', $this->acceptedMethods));
}
protected function honeyPot() : bool
+1 -1
View File
@@ -8,5 +8,5 @@ interface FormInterface
public function validate(FormContextInterface $context) : bool;
public function execute(FormContextInterface $context) : void;
public function execute(FormContextInterface $context);
}
+1
View File
@@ -4,4 +4,5 @@ namespace Picea\Ui\Method;
interface FormMessage {
public function isError() : bool;
# public function isSuccess() : bool; # Should be added in next major version
}
+8 -5
View File
@@ -7,9 +7,10 @@ use Picea\Extension\Extension,
use Picea\Compiler\Context;
use Picea\Extension\FunctionExtension;
use Psr\Http\Message\ServerRequestInterface;
class Pagination implements Extension {
class Pagination implements Extension, FunctionExtension {
use ExtensionTrait;
public array $tokens;
@@ -20,14 +21,16 @@ class Pagination implements Extension {
public function __construct(ServerRequestInterface $request, Context $context) {
$this->request = $request;
$this->register($context);
}
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) : string { }
public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token, array $options = []) : string { }
public function register(Context $context) : void
public function exportFunctions(): array
{
$context->pushFunction("pagination", [ $this, 'paginationClass' ]);
return [
"pagination" => [ $this, 'paginationClass' ]
];
}
public function paginationClass(string $name, ?string $url = null, ?array $params = null) : PaginationHandler
+1 -1
View File
@@ -41,7 +41,7 @@ class PaginationHandler {
}
public function __toString() {
return "hello world";
return "";
}
}
+4
View File
@@ -8,6 +8,9 @@ class Ui {
public function registerFormExtension(Compiler $compiler) : self
{
# BW compatibility with v1.x syntax
$compiler->registerExtension(new Form\Ui());
$compiler->registerExtension(new Form\UiForm());
$compiler->registerExtension(new Form\UiInput());
$compiler->registerExtension(new Form\UiCheckbox());
@@ -29,6 +32,7 @@ class Ui {
$compiler->registerExtension(new Form\UiUrl());
$compiler->registerExtension(new Form\UiWeek());
$compiler->registerExtension(new Form\UiSelect());
$compiler->registerExtension(new Form\UiImage());
$compiler->registerExtension(new Component\UiPopup());