{"action":"created","issue":{"url":"https://api.github.com/repos/Elgg/Elgg/issues/6808","labels_url":"https://api.github.com/repos/Elgg/Elgg/issues/6808/labels{/name}","comments_url":"https://api.github.com/repos/Elgg/Elgg/issues/6808/comments","events_url":"https://api.github.com/repos/Elgg/Elgg/issues/6808/events","html_url":"https://github.com/Elgg/Elgg/issues/6808","id":32589320,"number":6808,"title":"Declarative input validation and form generation","user":{"login":"ewinslow","id":356564,"avatar_url":"https://avatars.githubusercontent.com/u/356564?v=3","gravatar_id":"","url":"https://api.github.com/users/ewinslow","html_url":"https://github.com/ewinslow","followers_url":"https://api.github.com/users/ewinslow/followers","following_url":"https://api.github.com/users/ewinslow/following{/other_user}","gists_url":"https://api.github.com/users/ewinslow/gists{/gist_id}","starred_url":"https://api.github.com/users/ewinslow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ewinslow/subscriptions","organizations_url":"https://api.github.com/users/ewinslow/orgs","repos_url":"https://api.github.com/users/ewinslow/repos","events_url":"https://api.github.com/users/ewinslow/events{/privacy}","received_events_url":"https://api.github.com/users/ewinslow/received_events","type":"User","site_admin":false},"labels":[{"url":"https://api.github.com/repos/Elgg/Elgg/labels/feature","name":"feature","color":"DDDDDD"},{"url":"https://api.github.com/repos/Elgg/Elgg/labels/forms","name":"forms","color":"000000"}],"state":"open","locked":false,"assignee":null,"milestone":{"url":"https://api.github.com/repos/Elgg/Elgg/milestones/69","html_url":"https://github.com/Elgg/Elgg/milestones/Revamp%20forms/actions","labels_url":"https://api.github.com/repos/Elgg/Elgg/milestones/69/labels","id":833136,"number":69,"title":"Revamp forms/actions","description":"Major issues to solve:\r\n * Accessibility\r\n * Extensibility\r\n * Testability\r\n * Usability","creator":{"login":"ewinslow","id":356564,"avatar_url":"https://avatars.githubusercontent.com/u/356564?v=3","gravatar_id":"","url":"https://api.github.com/users/ewinslow","html_url":"https://github.com/ewinslow","followers_url":"https://api.github.com/users/ewinslow/followers","following_url":"https://api.github.com/users/ewinslow/following{/other_user}","gists_url":"https://api.github.com/users/ewinslow/gists{/gist_id}","starred_url":"https://api.github.com/users/ewinslow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ewinslow/subscriptions","organizations_url":"https://api.github.com/users/ewinslow/orgs","repos_url":"https://api.github.com/users/ewinslow/repos","events_url":"https://api.github.com/users/ewinslow/events{/privacy}","received_events_url":"https://api.github.com/users/ewinslow/received_events","type":"User","site_admin":false},"open_issues":14,"closed_issues":5,"state":"open","created_at":"2014-10-20T18:07:51Z","updated_at":"2015-05-30T00:37:03Z","due_on":null,"closed_at":null},"comments":11,"created_at":"2014-05-01T01:50:31Z","updated_at":"2015-07-11T01:50:42Z","closed_at":null,"body":"Rough thoughts on making Elgg a *rapid* social development framework...\r\n\r\n## Objective\r\n\r\nMake it easier to get started with developing/displaying/managing new content types.\r\n\r\n## Background\r\nJust having documentation on all the considerations would be helpful:\r\n 1. Think about the content I want to post (fields, types, constraints, etc.)\r\n 2. Create views for displaying that content\r\n   a. Landing page\r\n   b. Listing page\r\n\r\n 3. Create a form for creating/editing that content\r\n   a. Make sure this works for both creating and editing!\r\n   b. Implement auto-save/drafts support. Support sticky forms.\r\n\r\n 4. Implement the action that saves the new entity type\r\n   a. Do input validation\r\n   b. Handle input errors (missing container_guid, permissions, etc.)\r\n   c. Don't forget the river entry!\r\n\r\n 5. Implement page handlers that direct me to the different views\r\n    a. Create-a-new-one page\r\n    b. Save-an-existing-one page\r\n    b. List-some pages (in a group, on a user's profile, all across site, user's friends, etc.)\r\n\r\n 6. Provide language strings\r\n\r\n## Desired Features\r\nThis feature list was getting out of hand, so I decided to try breaking it down into logical milestones.\r\n\r\n### V1 (Declarative input validation)\r\nThis would greatly improve the current state of Elgg validation, which forces folks to do it in a hand-rolled way:\r\n* mostly declarative schema on the server, since the server is the source of truth for the validation rules of an action.\r\n* based on HTML5 input properties so that it is very accessible to web developers.\r\n* built with an eye toward form-generation\r\n* Should provide a way to add custom server-side validation rules\r\n* Should be possible to define custom input types that encapsulate several validation rules (e.g., for rendering autocomplete for entities).\r\n\r\nSomeone with knowledge of HTML5 forms should have no trouble picking up our API, and someone with knowledge of our API should be able to take that and apply it to HTML5 forms. That means we want to avoid too much abstraction (don't create a PHP class for every input type!).\r\n\r\nLots of the form validation frameworks out there have close-but-not-quite correspondence to HTML5 input properties and this makes things rather confusing, if you ask me.\r\n\r\n### V2 (Form generation)\r\n* Should be able to generate a sensible/usable HTML form that adheres to the defined schema\r\n* Should allow the developer to take over the rendering of the form so they can provide a custom layout if they so choose.\r\n* Should provide a standard way to output the rest of the fields that the developer hasn't rendered explicitly so that plugins can still extend the form and action cleanly\r\n* Should standardize language keys for translating labels, hints, errors, etc. for the form\r\n* Should provide an API for autocompleting and/or validating individual fields (validating/autocompleting the whole form every time an input changes would not be performant).\r\n* Should have a mechanism for filling in defaults if you're editing an existing entity\r\n\r\n### V3 (Usability FTW)\r\n* Should save form submission in localStorage to avoid data loss until the form completes successfully\r\n* Should de-dupe and prevent non-idempotent actions, including the case where the form submits successfully but the connection drops out and the client never gets a success response (so it appears to have timed out).\r\n\r\n## Prior Art\r\n\r\nA word of warning from the author of Drupal's Forms API: http://daemon.co.za/2012/05/dynamic-forms-json-schema/:\r\n\r\n> You simply do not need to have a system that automagically generates forms based on a declaritive control structures. In my Drupal days, one of my largest contributions was the Drupal Forms API that worked on similar principles, so this was a very difficult lesson for me to learn.\r\n\r\nThis is sobering. I think the main difference is that we want to try to avoid making our form generator do *everything* and really limit how it can specify the UI. It *can* provide something reasonable, but anything beyond that and the UI developer should just completely take over.\r\n\r\n* [JSON Schema][], [JSON Form][], etc.\r\n* [Django Forms][]\r\n* [Zend Forms][]\r\n\r\n\r\n[JSON Schema]: http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5\r\n[JSON Form]: https://github.com/joshfire/jsonform/wiki\r\n[Django Forms]: https://docs.djangoproject.com/en/dev/topics/forms/modelforms/\r\n[Zend Forms]: http://framework.zend.com/manual/2.3/en/modules/zend.form.intro.html\r\n\r\n\r\n\r\n## Proposal\r\nDoing all the work above feels like a lot, especially if you're just starting out. We should provide a reasonable API so that people can start using and testing out their forms, validation rules, and business logic independently of the designers designing the form.\r\n\r\nIf I just say:\r\n\r\n```php\r\nreturn array(\r\n  'object:book' => array(\r\n    'fields' => array(\r\n      'cover' => array(\r\n        'type' => 'image',\r\n      ),\r\n      'title' => array(\r\n        'type' => 'text',\r\n        'required' => true,\r\n      ),\r\n      'authors' => array(\r\n        'type' => 'text',\r\n        'multiple' => true,\r\n      ),\r\n    ),\r\n  )\r\n);\r\n```\r\n\r\nThen it feels like Elgg should be able to do the other 95% of the work and let me get on my way posting books with associated authors and a cover image. The default UI doesn't have to be stellar, but it should be basic enough to start saving content to the DB.\r\n\r\nThe really interesting stuff is how this is all presented (UI) and we can't really automatically make that awesome. But having *something* to work with right away makes it much easier for people to think about that stuff, or work in parallel with the folks who are thinking about that stuff.\r\n\r\nIt turns out we already do this in a non-standard way for groups:\r\n\r\nhttps://github.com/Elgg/Elgg/blob/master/mod/groups/start.php#L122\r\n\r\n## Examples\r\n\r\n### Objects\r\n```php\r\nreturn array(\r\n  'object:default' => array(\r\n    'fields' => array(\r\n      'title' => array('type' => 'text'),\r\n      'description' => array('type' => 'html'),\r\n      'access_id' => array('type' => 'access'),\r\n      'container_guid' => array('type' => 'entity'),\r\n      'tags' => array(\r\n        'type' => 'text', \r\n        'multiple' => true, \r\n        'required' => false\r\n      ),\r\n    ),\r\n  ),\r\n);\r\n```\r\n\r\n### Blogs\r\n```php\r\nreturn array(\r\n  'object:blog' => array(\r\n    'extends' => 'object:default',\r\n    'fields' => array(\r\n      'status' => array(\r\n        'type' => 'enum',\r\n        'options' => array('draft', 'published'),\r\n      ),\r\n      'comments_on' => array('type' => 'bool'),\r\n      'excerpt' => array(\r\n        'type' => 'text', \r\n        'maxlength' => 250, \r\n        'required' => false\r\n      ),\r\n    ),\r\n  ),\r\n);\r\n```\r\n\r\n### Bookmarks\r\n```php\r\nreturn array(\r\n  'object:bookmarks' => array(\r\n    'extends' => 'object:default',\r\n    'fields' => array(\r\n      'address' => array('type' => 'url'),\r\n    ),\r\n  ).\r\n);\r\n```\r\n\r\n### Files\r\n```php\r\nreturn array(\r\n  'object:file' => array(\r\n    'extends' => 'object:default',\r\n    'fields' => array(\r\n      'simpletype' => array(\r\n        'type' => 'enum',\r\n        'options' => array(...)\r\n      ),\r\n    ),\r\n  ),\r\n);\r\n```\r\n\r\n### Groups\r\n```php\r\nreturn array(\r\n  'group:default' => array(\r\n    'fields' => array(\r\n      'description' => array('type' => 'html'),\r\n      'briefdescription' => array('type' => 'text'),\r\n      'interests' => array(\r\n        'type' => 'text',\r\n        'multiple' => true,\r\n        'required' => false\r\n      ),\r\n    ),\r\n  ),\r\n);\r\n```"},"comment":{"url":"https://api.github.com/repos/Elgg/Elgg/issues/comments/120562322","html_url":"https://github.com/Elgg/Elgg/issues/6808#issuecomment-120562322","issue_url":"https://api.github.com/repos/Elgg/Elgg/issues/6808","id":120562322,"user":{"login":"ewinslow","id":356564,"avatar_url":"https://avatars.githubusercontent.com/u/356564?v=3","gravatar_id":"","url":"https://api.github.com/users/ewinslow","html_url":"https://github.com/ewinslow","followers_url":"https://api.github.com/users/ewinslow/followers","following_url":"https://api.github.com/users/ewinslow/following{/other_user}","gists_url":"https://api.github.com/users/ewinslow/gists{/gist_id}","starred_url":"https://api.github.com/users/ewinslow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ewinslow/subscriptions","organizations_url":"https://api.github.com/users/ewinslow/orgs","repos_url":"https://api.github.com/users/ewinslow/repos","events_url":"https://api.github.com/users/ewinslow/events{/privacy}","received_events_url":"https://api.github.com/users/ewinslow/received_events","type":"User","site_admin":false},"created_at":"2015-07-11T01:50:42Z","updated_at":"2015-07-11T01:50:42Z","body":"And of course, lest I forget, the translation string *keys* would be automatically generated based on this metadata. Authors would have to fill in the metadata for the ones they want to show up."}}