From d9e306b641509ab901d7800f6dc20354a9e6c297 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 9 Jan 2018 19:10:56 +0530 Subject: [PATCH] added forms + controls --- frappe/client/index.js | 23 +++++++ frappe/client/ui/index.js | 33 ++++++++++ frappe/client/view/controls/base.js | 90 ++++++++++++++++++++++++++++ frappe/client/view/controls/data.js | 10 ++++ frappe/client/view/controls/index.js | 16 +++++ frappe/client/view/form.js | 66 ++++++++++++++++++++ frappe/client/view/index.js | 20 +++++++ frappe/{ => client}/view/list.js | 11 ++-- frappe/client/view/page.js | 24 ++++++++ frappe/client/view/router.js | 82 +++++++++++++++++++++++++ frappe/index.js | 16 ----- frappe/model/document.js | 4 ++ frappe/model/meta.js | 34 +++++++++-- frappe/models/doctype/todo/todo.js | 13 ++-- frappe/server/index.js | 66 ++++++++++---------- frappe/view/index.js | 6 -- frappe/view/router.js | 0 17 files changed, 441 insertions(+), 73 deletions(-) create mode 100644 frappe/client/index.js create mode 100644 frappe/client/ui/index.js create mode 100644 frappe/client/view/controls/base.js create mode 100644 frappe/client/view/controls/data.js create mode 100644 frappe/client/view/controls/index.js create mode 100644 frappe/client/view/form.js create mode 100644 frappe/client/view/index.js rename frappe/{ => client}/view/list.js (68%) create mode 100644 frappe/client/view/page.js create mode 100644 frappe/client/view/router.js delete mode 100644 frappe/view/index.js delete mode 100644 frappe/view/router.js diff --git a/frappe/client/index.js b/frappe/client/index.js new file mode 100644 index 00000000..1b9ee46c --- /dev/null +++ b/frappe/client/index.js @@ -0,0 +1,23 @@ +const common = require('frappe-core/frappe/common'); +const Database = require('frappe-core/frappe/backends/rest_client').Database; +const frappe = require('frappe-core'); +frappe.ui = require('./ui'); +frappe.view = require('./view'); +const Router = require('./view/router').Router; + +module.exports = { + async start({server, container}) { + window.frappe = frappe; + frappe.init(); + common.init_libs(frappe); + + frappe.db = await new Database({ + server: server, + fetch: window.fetch.bind() + }); + + frappe.view.init({container: container}); + frappe.router = new Router(); + } +}; + diff --git a/frappe/client/ui/index.js b/frappe/client/ui/index.js new file mode 100644 index 00000000..5857c99d --- /dev/null +++ b/frappe/client/ui/index.js @@ -0,0 +1,33 @@ +const frappe = require('frappe-core'); + +module.exports = { + add(tag, className, parent) { + let element = document.createElement(tag); + if (className) { + for (let c of className.split(' ')) { + this.add_class(element, c); + } + } + if (parent) { + parent.appendChild(element); + } + return element; + }, + + add_class(element, className) { + if (element.classList) { + element.classList.add(className); + } else { + element.className += " " + className; + } + }, + + remove_class(element, className) { + if (element.classList) { + element.classList.remove(className); + } else { + element.className = element.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); + } + } + +} \ No newline at end of file diff --git a/frappe/client/view/controls/base.js b/frappe/client/view/controls/base.js new file mode 100644 index 00000000..18c5497a --- /dev/null +++ b/frappe/client/view/controls/base.js @@ -0,0 +1,90 @@ +const frappe = require('frappe-core'); + +class BaseControl { + constructor(docfield, parent) { + Object.assign(this, docfield); + if (!this.fieldname) { + this.fieldname = frappe.slug(this.label); + } + this.parent = parent; + if (this.setup) { + this.setup(); + } + } + + bind(doc) { + this.doc = doc; + + this.doc.add_handler(this.fieldname, () => { + this.set_doc_value(); + }); + + this.set_doc_value(); + } + + refresh() { + this.make(); + this.set_doc_value(); + } + + set_doc_value() { + if (this.doc) { + this.set_input_value(this.doc.get(this.fieldname)); + } + } + + make() { + if (!this.form_group) { + this.make_form_group(); + this.make_label(); + this.make_input(); + this.make_description(); + this.bind_change_event(); + } + } + + make_form_group() { + this.form_group = frappe.ui.add('div', 'form-group', this.parent); + } + + make_label() { + this.label_element = frappe.ui.add('label', null, this.form_group); + this.label_element.textContent = this.label; + } + + make_input() { + this.input = frappe.ui.add('input', 'form-control', this.form_group); + this.input.setAttribute('type', this.fieldname); + } + + make_description() { + if (this.description) { + this.description_element = frappe.ui.add('small', 'form-text text-muted', this.form_group); + this.description_element.textContent = this.description; + } + } + + set_input_value(value) { + this.input.value = value; + } + + async get_input_value() { + return await this.parse(this.input.value); + } + + async parse(value) { + return value; + } + + bind_change_event() { + this.input.addEventListener('change', () => this.handle_change(e)); + } + + async handle_change(e) { + let value = await this.get_input_value(); + value = await this.validate(value); + await this.doc.set(this.fieldname, value); + } +} + +module.exports = BaseControl; \ No newline at end of file diff --git a/frappe/client/view/controls/data.js b/frappe/client/view/controls/data.js new file mode 100644 index 00000000..4a967f31 --- /dev/null +++ b/frappe/client/view/controls/data.js @@ -0,0 +1,10 @@ +const BaseControl = require('./base'); + +class DataControl extends BaseControl { + make() { + super.make(); + this.input.setAttribute('type', 'text'); + } +}; + +module.exports = DataControl; \ No newline at end of file diff --git a/frappe/client/view/controls/index.js b/frappe/client/view/controls/index.js new file mode 100644 index 00000000..1d2bf5d9 --- /dev/null +++ b/frappe/client/view/controls/index.js @@ -0,0 +1,16 @@ +const control_classes = { + Data: require('./data') +} + + +module.exports = { + get_control_class(fieldtype) { + return control_classes[fieldtype]; + }, + make_control(field, parent) { + const control_class = this.get_control_class(field.fieldtype); + let control = new control_class(field, parent); + control.make(); + return control; + } +} \ No newline at end of file diff --git a/frappe/client/view/form.js b/frappe/client/view/form.js new file mode 100644 index 00000000..d5b90590 --- /dev/null +++ b/frappe/client/view/form.js @@ -0,0 +1,66 @@ +const frappe = require('frappe-core'); +const controls = require('./controls'); + +class Form { + constructor({doctype, parent, submit_label='Submit'}) { + this.parent = parent; + this.doctype = doctype; + this.submit_label = submit_label; + + this.controls = {}; + this.controls_list = []; + + this.meta = frappe.get_meta(this.doctype); + } + + make() { + this.body = frappe.ui.add('form', null, this.parent); + for(let df of this.meta.fields) { + if (controls.get_control_class(df.fieldtype)) { + let control = controls.make_control(df, this.body); + this.controls_list.push(control); + this.controls[df.fieldname] = control; + } + } + this.make_submit(); + } + + make_submit() { + this.submit_btn = frappe.ui.add('button', 'btn btn-primary', this.body); + this.submit_btn.setAttribute('type', 'submit'); + this.submit_btn.textContent = this.submit_label; + this.submit_btn.addEventListener('click', (event) => { + this.submit(); + }) + } + + async use(doc, is_new = false) { + if (this.doc) { + // clear handlers of outgoing doc + this.doc.clear_handlers(); + } + this.doc = doc; + this.is_new = is_new; + for (let control of this.controls_list) { + control.bind(this.doc); + } + } + + async submit() { + if (this.is_new) { + await this.doc.insert(); + } else { + await this.doc.update(); + } + await this.refresh(); + } + + refresh() { + for(let control of this.controls_list) { + control.refresh(); + } + } + +} + +module.exports = {Form: Form}; \ No newline at end of file diff --git a/frappe/client/view/index.js b/frappe/client/view/index.js new file mode 100644 index 00000000..91d18132 --- /dev/null +++ b/frappe/client/view/index.js @@ -0,0 +1,20 @@ +const frappe = require('frappe-core'); + +module.exports = { + init({container, main, sidebar}) { + frappe.container = container; + + if (sidebar) { + frappe.sidebar = sidebar; + } else { + frappe.sidebar = frappe.ui.add('div', 'sidebar', frappe.container); + } + + if (main) { + frappe.main = main; + } else { + frappe.main = frappe.ui.add('div', 'main', frappe.container); + } + }, + +} \ No newline at end of file diff --git a/frappe/view/list.js b/frappe/client/view/list.js similarity index 68% rename from frappe/view/list.js rename to frappe/client/view/list.js index f3bfcb19..43d21a58 100644 --- a/frappe/view/list.js +++ b/frappe/client/view/list.js @@ -13,9 +13,9 @@ class ListView { this.rows = []; } - async render() { + async run() { this.make_body(); - let data = await this.meta.get_list(this.start, this.page_length); + let data = await this.meta.get_list({start:this.start, limit:this.page_length}); for (let i=0; i< data.length; i++) { this.render_row(this.start + i, data[i]); @@ -24,19 +24,18 @@ class ListView { make_body() { if (!this.body) { - this.body = $('
') - .appendTo(frappe.main); + this.body = frappe.ui.add('div', 'list-body', this.parent); } } render_row(i, data) { let row = this.get_row(i); - row.html(this.meta.get_row_html(data)); + row.innerHTML = this.meta.get_row_html(data); } get_row(i) { if (!this.rows[i]) { - this.rows[i] = $('
').appendTo(this.body); + this.rows[i] = frappe.ui.add('div', 'list-row', this.body); } return this.rows[i]; } diff --git a/frappe/client/view/page.js b/frappe/client/view/page.js new file mode 100644 index 00000000..ea962693 --- /dev/null +++ b/frappe/client/view/page.js @@ -0,0 +1,24 @@ +const frappe = require('frappe-core'); + +class Page { + constructor(title) { + this.title = title; + this.make(); + } + make() { + this.body = frappe.ui.add('div', 'page hide', frappe.main); + } + hide() { + frappe.ui.add_class(this.body, 'hide'); + } + show() { + if (frappe.router.current_page) { + frappe.router.current_page.hide(); + } + frappe.ui.remove_class(this.body, 'hide'); + frappe.router.current_page = this; + document.title = this.title; + } +} + +module.exports = { Page: Page }; \ No newline at end of file diff --git a/frappe/client/view/router.js b/frappe/client/view/router.js new file mode 100644 index 00000000..bba245f2 --- /dev/null +++ b/frappe/client/view/router.js @@ -0,0 +1,82 @@ +const frappe = require('frappe-core'); + +class Router { + constructor() { + this.current_page = null; + this.routes = {}; + this.listen(); + } + + add(route, handler) { + let page = {handler: handler}; + + // '/todo/:name/:place'.match(/:([^/]+)/g); + page.param_keys = route.match(/:([^/]+)/g); + + if (page.param_keys) { + // make expression + // '/todo/:name/:place'.replace(/\/:([a-z1-9]+)/g, "\/([a-z0-9]+)"); + page.expression = route.replace(/\/:([a-z1-9]+)/g, "\/([a-z0-9]+)"); + } + + this.routes[route] = page; + } + + listen() { + window.onhashchange = this.changed.bind(this); + this.changed(); + } + + async changed(event) { + if (window.location.hash.length > 0) { + const page_name = window.location.hash.substr(1); + this.show(page_name); + } else if (this.routes['default']) { + this.show('default'); + } + } + + show(route) { + if (!route) { + route = 'default'; + } + + if (route[0]==='#') { + route = route.substr(1); + } + + let page = this.match(route); + + if (page) { + if (typeof page.handler==='function') { + page.handler(page.params); + } else { + page.handler.show(page.params); + } + } + } + + match(route) { + for(let key in this.routes) { + let page = this.routes[key]; + + if (page.param_keys) { + let matches = route.match(new RegExp(page.expression)); + if (matches && matches.length == page.param_keys.length + 1) { + let params = {} + for (let i=0; i < page.param_keys.length; i++) { + params[page.param_keys[i].substr(1)] = matches[i + 1]; + } + return {handler:page.handler, params: params}; + } + + } else { + if (key === route) { + return {handler:page.handler}; + } + } + } + } +} + +module.exports = {Router: Router}; \ No newline at end of file diff --git a/frappe/index.js b/frappe/index.js index 7f3a2c2c..aeea1b0c 100644 --- a/frappe/index.js +++ b/frappe/index.js @@ -22,22 +22,6 @@ module.exports = { this.meta_cache = {}; }, - init_view({container, main, sidebar}) { - this.container = container; - - if (sidebar) { - this.sidebar = sidebar; - } else { - this.sidebar = $('').appendTo(this.container); - } - - if (main) { - this.main = main; - } else { - this.main = $('
').appendTo(this.container); - } - }, - get_meta(doctype) { if (!this.meta_cache[doctype]) { this.meta_cache[doctype] = new (this.models.get_meta_class(doctype))(this.models.get('DocType', doctype)); diff --git a/frappe/model/document.js b/frappe/model/document.js index fd756ae7..71ab72e3 100644 --- a/frappe/model/document.js +++ b/frappe/model/document.js @@ -11,6 +11,10 @@ class Document { // add handlers } + clear_handlers() { + this.handlers = {}; + } + add_handler(key, method) { if (!this.handlers[key]) { this.handlers[key] = []; diff --git a/frappe/model/meta.js b/frappe/model/meta.js index fba80839..e1672add 100644 --- a/frappe/model/meta.js +++ b/frappe/model/meta.js @@ -5,6 +5,12 @@ class Meta extends Document { constructor(data) { super(data); this.event_handlers = {}; + this.list_options = { + fields: ['name', 'modified'] + }; + if (this.setup_meta) { + this.setup_meta(); + } } get_field(fieldname) { @@ -24,6 +30,15 @@ class Meta extends Document { this.event_handlers[key].push(fn); } + async set(fieldname, value) { + this[fieldname] = value; + await this.trigger(fieldname); + } + + get(fieldname) { + return this[fieldname]; + } + get_valid_fields() { if (!this._valid_fields) { this._valid_fields = []; @@ -62,15 +77,26 @@ class Meta extends Document { } } - trigger(key) { + async trigger(key, event = {}) { + Object.assign(event, { + doc: this, + name: key + }); + + if (this.event_handlers[key]) { + for (var handler of this.event_handlers[key]) { + await handler(event); + } + } } - // views - async get_list(start, limit=20) { + // collections + async get_list({start, limit=20, filters}) { return await frappe.db.get_all({ doctype: this.name, - fields: ['name'], + fields: this.list_options.fields, + filters: filters, start: start, limit: limit }); diff --git a/frappe/models/doctype/todo/todo.js b/frappe/models/doctype/todo/todo.js index e848dc47..3a163c16 100644 --- a/frappe/models/doctype/todo/todo.js +++ b/frappe/models/doctype/todo/todo.js @@ -1,17 +1,14 @@ const frappe = require('frappe-core'); class todo_meta extends frappe.meta.Meta { - async get_list(start, limit=20) { - return await frappe.db.get_all({ - doctype: 'ToDo', - fields: ['name', 'subject', 'status', 'description'], - start: start, - limit: limit - }); + setup_meta() { + Object.assign(this, require('./todo.json')); + this.name = 'ToDo'; + this.list_options.fields = ['name', 'subject', 'status', 'description']; } get_row_html(data) { - return `${data.subject}`; + return `${data.subject}`; } } diff --git a/frappe/server/index.js b/frappe/server/index.js index 1d22406a..454eb726 100644 --- a/frappe/server/index.js +++ b/frappe/server/index.js @@ -9,37 +9,37 @@ const models = require('frappe-core/frappe/server/models'); const common = require('frappe-core/frappe/common'); const bodyParser = require('body-parser'); -async function init({backend, connection_params}) { - await frappe.init(); - common.init_libs(frappe); - await frappe.login(); - - // walk and find models - models.init(); - - // database - - frappe.db = await new backends[backend].Database(connection_params); - await frappe.db.connect(); - await frappe.db.migrate(); -} - -async function start({backend, connection_params}) { - await init({backend, connection_params}); - - // app - app.use(bodyParser.json()); - app.use(bodyParser.urlencoded({ extended: true })); - - // routes - rest_server.setup(app); - - // listen - frappe.app = app; - frappe.server = app.listen(frappe.config.port); -} - module.exports = { - init: init, - start: start -} \ No newline at end of file + async init() { + await frappe.init(); + common.init_libs(frappe); + await frappe.login(); + + // walk and find models + models.init(); + + }, + + async start({backend, connection_params, static}) { + await this.init(); + + // database + frappe.db = await new backends[backend].Database(connection_params); + await frappe.db.connect(); + await frappe.db.migrate(); + + // app + app.use(bodyParser.json()); + app.use(bodyParser.urlencoded({ extended: true })); + app.use(express.static('./')); + + // routes + rest_server.setup(app); + + // listen + frappe.app = app; + frappe.server = app.listen(frappe.config.port); + + } +} + diff --git a/frappe/view/index.js b/frappe/view/index.js deleted file mode 100644 index 042f2488..00000000 --- a/frappe/view/index.js +++ /dev/null @@ -1,6 +0,0 @@ -const frappe = require('frappe-core'); - -module.exports = { - setup() { - } -} \ No newline at end of file diff --git a/frappe/view/router.js b/frappe/view/router.js deleted file mode 100644 index e69de29b..00000000