Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
Monster
Manage
Activity
Members
Plan
Jira
Code
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Analyze
Contributor analytics
Help
Help
Support
GitLab documentation
Compare GitLab plans
GitLab community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
OSS
Libraries
Javascript
Monster
Commits
b835d2da
Verified
Commit
b835d2da
authored
1 year ago
by
Volker Schukai
Browse files
Options
Downloads
Patches
Plain Diff
feat: complete change of form control to a derivation of dataset #216
parent
fb796880
No related branches found
No related tags found
No related merge requests found
Changes
1
Show whitespace changes
Inline
Side-by-side
Showing
1 changed file
source/components/form/form.mjs
+192
-544
192 additions, 544 deletions
source/components/form/form.mjs
with
192 additions
and
544 deletions
source/components/form/form.mjs
+
192
−
544
View file @
b835d2da
...
@@ -14,7 +14,10 @@
...
@@ -14,7 +14,10 @@
import
{
instanceSymbol
}
from
"
../../constants.mjs
"
;
import
{
instanceSymbol
}
from
"
../../constants.mjs
"
;
import
{
internalSymbol
}
from
"
../../constants.mjs
"
;
import
{
internalSymbol
}
from
"
../../constants.mjs
"
;
import
{
Datasource
}
from
"
../../data/datasource.mjs
"
;
import
{
TokenList
}
from
"
../../types/tokenlist.mjs
"
;
import
{
DeadMansSwitch
}
from
"
../../util/deadmansswitch.mjs
"
;
import
{
DataSet
}
from
"
../datatable/dataset.mjs
"
;
//import { Datasource } from "../../data/datasource.mjs";
import
{
RestAPI
}
from
"
../../data/datasource/server/restapi.mjs
"
;
import
{
RestAPI
}
from
"
../../data/datasource/server/restapi.mjs
"
;
import
{
WebConnect
}
from
"
../../data/datasource/server/webconnect.mjs
"
;
import
{
WebConnect
}
from
"
../../data/datasource/server/webconnect.mjs
"
;
import
{
WriteError
}
from
"
../../data/datasource/server/restapi/writeerror.mjs
"
;
import
{
WriteError
}
from
"
../../data/datasource/server/restapi/writeerror.mjs
"
;
...
@@ -36,10 +39,12 @@ import {
...
@@ -36,10 +39,12 @@ import {
getSlottedElements
,
getSlottedElements
,
}
from
"
../../dom/customelement.mjs
"
;
}
from
"
../../dom/customelement.mjs
"
;
import
{
addObjectWithUpdaterToElement
}
from
"
../../dom/updater.mjs
"
;
import
{
addObjectWithUpdaterToElement
}
from
"
../../dom/updater.mjs
"
;
import
{
findElementWithSelectorUpwards
}
from
"
../../dom/util.mjs
"
;
import
{
isFunction
,
isString
}
from
"
../../types/is.mjs
"
;
import
{
isFunction
,
isString
}
from
"
../../types/is.mjs
"
;
import
{
Observer
}
from
"
../../types/observer.mjs
"
;
import
{
Observer
}
from
"
../../types/observer.mjs
"
;
import
{
ProxyObserver
}
from
"
../../types/proxyobserver.mjs
"
;
import
{
ProxyObserver
}
from
"
../../types/proxyobserver.mjs
"
;
import
{
Processing
}
from
"
../../util/processing.mjs
"
;
import
{
Processing
}
from
"
../../util/processing.mjs
"
;
import
{
datasourceLinkedElementSymbol
,
handleDataSourceChanges
}
from
"
../datatable/util.mjs
"
;
import
{
MessageStateButton
}
from
"
./message-state-button.mjs
"
;
import
{
MessageStateButton
}
from
"
./message-state-button.mjs
"
;
import
{
import
{
ATTRIBUTE_FORM_DATASOURCE
,
ATTRIBUTE_FORM_DATASOURCE
,
...
@@ -50,217 +55,50 @@ import { FormStyleSheet } from "./stylesheet/form.mjs";
...
@@ -50,217 +55,50 @@ import { FormStyleSheet } from "./stylesheet/form.mjs";
export
{
Form
};
export
{
Form
};
/**
* @private
* @since 3.1.0
* @type {string}
*/
const
ATTRIBUTE_FORM_DATASOURCE_ACTION
=
`
${
ATTRIBUTE_PREFIX
}
datasource-action`
;
/**
* Form data is the internal representation of the form data
*
* @private
* @type {symbol}
* @since 1.7.0
*/
const
formDataSymbol
=
Symbol
.
for
(
"
@schukai/monster/components/form/form@@formdata
"
,
);
/**
* @private
* @type {symbol}
* @since 2.8.0
*/
const
formDataUpdaterSymbol
=
Symbol
.
for
(
"
@schukai/component-form/form@@formdata-updater-link
"
,
);
/**
* @private
* @type {symbol}
* @since 1.7.0
*/
const
formElementSymbol
=
Symbol
.
for
(
"
@schukai/component-form/form@@form-element
"
,
);
/**
/**
* @private
* @private
* @type {symbol}
* @type {symbol}
* @since 2.5.0
*/
const
registeredDatasourcesSymbol
=
Symbol
.
for
(
"
@schukai/component-form/form@@registered-datasources
"
,
);
/**
* @private
* @since 1.7.0
* @type {string}
*/
const
PROPERTY_VALIDATION_KEY
=
"
__validation
"
;
/**
* This CustomControl creates a form element with a variety of options.
*
* <img src="./images/form.png">
*
* Dependencies: the system uses functions of the [monsterjs](https://monsterjs.org/) library.
*
* You can create this control either by specifying the HTML tag `<monster-form />` directly in the HTML or using
* Javascript via the `document.createElement('monster-form');` method.
*
* ```html
* <monster-form></monster-form>
* ```
*
* Or you can create this CustomControl directly in Javascript:
*
* ```js
* import {Form} from '@schukai/component-form/source/form.js';
* document.createElement('monster-form');
* ```
*
* @startuml form.png
* skinparam monochrome true
* skinparam shadowing false
* HTMLElement <|-- CustomElement
* CustomElement <|-- Form
* @enduml
*
* @since 1.6.0
* @copyright schukai GmbH
* @memberOf Monster.Components.Form
* @summary A configurable form control
*/
class
Form
extends
CustomElement
{
/**
* @throws {Error} the options attribute does not contain a valid json definition.
* @since 1.7.0
*/
*/
constructor
()
{
const
debounceCallbackSymbol
=
Symbol
(
"
timerCallback
"
);
super
();
this
[
formDataSymbol
]
=
new
ProxyObserver
({});
}
/**
class
Form
extends
DataSet
{
* This method is called by the `instanceof` operator.
* @returns {symbol}
* @since 2.1.0
*/
static
get
[
instanceSymbol
]()
{
return
Symbol
.
for
(
"
@schukai/monster/components/form/form
"
);
}
/**
/**
* To set the options via the html tag the attribute `data-monster-options` must be used.
* @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
*
* The individual configuration values can be found in the table.
*
*
* @property {Object} templates Template definitions
* @returns {{shadowMode: string, templates: {main: *}, display: string, disabled: boolean, delegatesFocus: boolean, templateMapping: {}} & {templates: {main: string}, classes: {form: string}}}
* @property {string} templates.main Main template
* @property {Datasource} datasource data source
* @property {Object} reportValidity
* @property {string} reportValidity.selector which element should be used to report the validity
* @property {function} reportValidity.errorHandler function to handle the error
* @property {Object} classes
* @property {string} classes.button class for the form
*/
*/
get
defaults
()
{
get
defaults
()
{
return
Object
.
assign
(
const
obj
=
Object
.
assign
(
{},
{},
super
.
defaults
,
super
.
defaults
,
{
{
templates
:
{
templates
:
{
main
:
getTemplate
(),
main
:
getTemplate
(),
},
},
datasource
:
undefined
,
reportValidity
:
{
selector
:
"
input,select,textarea
"
,
errorHandler
:
undefined
,
},
classes
:
{
classes
:
{
form
:
""
,
form
:
""
,
},
},
writeBack
:
{
events
:
[
"
change
"
,
"
input
"
,
"
keyup
"
]
},
},
initOptionsFromArguments
.
call
(
this
),
);
}
/**
reportValidity
:
{
* Called every time the element is inserted into the DOM. Useful for running setup code, such as
selector
:
"
input,select,textarea
"
,
* fetching resources or rendering. Generally, you should try to delay work until this time.
*
* @return {void}
*/
connectedCallback
()
{
super
[
"
connectedCallback
"
]();
}
}
/**
* The refresh method is called to update the control after a change with fresh data.
*
* Therefore, the data source is called again and the data is updated.
*
* If you have updated the data source with `setOption('datasource',datasource), you must call this method.
*
* @return {Form}
* @throws {Error} undefined datasource
*/
refresh
()
{
try
{
this
.
setAttribute
(
ATTRIBUTE_DISABLED
,
""
);
const
datasource
=
this
.
getOption
(
"
datasource
"
);
if
(
!
(
datasource
instanceof
Datasource
))
{
throw
new
Error
(
"
undefined datasource
"
);
}
}
);
return
datasource
obj
[
'
features
'
][
'
mutationObserver
'
]
=
false
;
.
read
()
obj
[
'
features
'
][
'
writeBack
'
]
=
true
;
.
then
(()
=>
{
this
[
formDataSymbol
].
setSubject
(
datasource
.
get
());
})
.
then
(()
=>
{
new
Processing
(()
=>
{
this
.
removeAttribute
(
ATTRIBUTE_DISABLED
);
}).
run
();
})
.
catch
((
e
)
=>
{
this
.
removeAttribute
(
ATTRIBUTE_DISABLED
);
this
.
setAttribute
(
ATTRIBUTE_ERRORMESSAGE
,
e
.
toString
());
});
}
catch
(
e
)
{
this
.
setAttribute
(
ATTRIBUTE_ERRORMESSAGE
,
e
.
toString
());
this
.
removeAttribute
(
ATTRIBUTE_DISABLED
);
throw
e
;
}
}
/**
return
obj
;
*
* @return {Monster.Components.Form.Form}
*/
[
assembleMethodSymbol
]()
{
super
[
assembleMethodSymbol
]();
initControlReferences
.
call
(
this
);
initDatasource
.
call
(
this
);
initUpdater
.
call
(
this
);
initObserver
.
call
(
this
);
return
this
;
}
}
/**
*
* @return {*}
*/
getValues
()
{
return
this
[
formDataSymbol
].
getSubject
();
}
/**
/**
*
*
...
@@ -271,46 +109,33 @@ class Form extends CustomElement {
...
@@ -271,46 +109,33 @@ class Form extends CustomElement {
}
}
/**
/**
*
* @return {CSSStyleSheet[]}
* @return {CSSStyleSheet}
*/
*/
static
getCSSStyleSheet
()
{
static
getCSSStyleSheet
()
{
return
[
FormStyleSheet
];
return
[
FormStyleSheet
];
}
}
static
[
registeredDatasourcesSymbol
]
=
new
Map
([
[
"
restapi
"
,
RestAPI
],
[
"
localstorage
"
,
LocalStorage
],
[
"
sessionstorage
"
,
SessionStorage
],
[
"
webconnect
"
,
WebConnect
],
]);
/**
/**
* Register a new datasource
*
*
* @param {string} name
* @param {Monster.Data.Datasource} datasource
*/
*/
static
registerDatasource
(
name
,
datasource
)
{
[
assembleMethodSymbol
]()
{
Form
[
registeredDatasourcesSymbol
].
set
(
name
,
datasource
);
super
[
assembleMethodSymbol
]();
}
initControlReferences
.
call
(
this
);
initEventHandler
.
call
(
this
);
initDataSourceHandler
.
call
(
this
);
/**
* Unregister a registered datasource
*
* @param {string} name
*/
static
unregisterDatasource
(
name
)
{
Form
[
registeredDatasourcesSymbol
].
delete
(
name
);
}
}
/**
/**
* Get registered data sources
* This method is called when the component is created.
*
* @since 3.70.0
* @return {
Map
}
* @return
s
{
DataSet
}
*/
*/
static
getDatasources
()
{
refresh
()
{
return
Form
[
registeredDatasourcesSymbol
];
this
.
write
();
super
.
refresh
();
return
this
;
}
}
/**
/**
...
@@ -324,6 +149,7 @@ class Form extends CustomElement {
...
@@ -324,6 +149,7 @@ class Form extends CustomElement {
const
selector
=
this
.
getOption
(
"
reportValidity.selector
"
);
const
selector
=
this
.
getOption
(
"
reportValidity.selector
"
);
const
nodes
=
getSlottedElements
.
call
(
this
,
selector
);
const
nodes
=
getSlottedElements
.
call
(
this
,
selector
);
nodes
.
forEach
((
node
)
=>
{
nodes
.
forEach
((
node
)
=>
{
if
(
typeof
node
.
reportValidity
===
"
function
"
)
{
if
(
typeof
node
.
reportValidity
===
"
function
"
)
{
if
(
node
.
reportValidity
()
===
false
)
{
if
(
node
.
reportValidity
()
===
false
)
{
...
@@ -334,253 +160,75 @@ class Form extends CustomElement {
...
@@ -334,253 +160,75 @@ class Form extends CustomElement {
return
valid
;
return
valid
;
}
}
}
/**
* @private
*/
function
initUpdater
()
{
if
(
!
this
.
shadowRoot
)
{
throw
new
Error
(
"
no shadow-root is defined
"
);
}
const
slots
=
this
.
shadowRoot
.
querySelectorAll
(
"
slot
"
);
for
(
const
[,
slot
]
of
Object
.
entries
(
slots
))
{
for
(
const
[,
node
]
of
Object
.
entries
(
slot
.
assignedNodes
()))
{
if
(
!
(
node
instanceof
HTMLElement
))
{
continue
;
}
const
query
=
`[
${
ATTRIBUTE_UPDATER_ATTRIBUTES
}
],[
${
ATTRIBUTE_UPDATER_REPLACE
}
],[
${
ATTRIBUTE_UPDATER_REMOVE
}
],[
${
ATTRIBUTE_UPDATER_INSERT
}
]`
;
const
controls
=
node
.
querySelectorAll
(
query
);
const
list
=
new
Set
([...
controls
]);
if
(
node
.
matches
(
query
))
{
list
.
add
(
node
);
}
}
if
(
list
.
size
===
0
)
{
function
initDataSourceHandler
()
{
continue
;
if
(
!
this
[
datasourceLinkedElementSymbol
])
{
return
;
}
}
console
.
log
(
this
[
datasourceLinkedElementSymbol
]);
this
[
datasourceLinkedElementSymbol
].
setOption
(
"
write.responseCallback
"
,
(
response
)
=>
{
console
.
log
(
"
response!!!
"
,
response
);
})
addObjectWithUpdaterToElement
.
call
(
node
,
list
,
formDataUpdaterSymbol
,
this
[
formDataSymbol
],
);
}
}
}
}
/**
/**
* @private
* @private
* @returns {initEventHandler}
*/
*/
function
initDatasource
()
{
function
initEventHandler
()
{
if
(
!
this
.
shadowRoot
)
{
throw
new
Error
(
"
no shadow-root is defined
"
);
}
const
slots
=
this
.
shadowRoot
.
querySelectorAll
(
"
slot
"
);
for
(
const
[,
slot
]
of
Object
.
entries
(
slots
))
{
for
(
const
[,
node
]
of
Object
.
entries
(
slot
.
assignedNodes
()))
{
if
(
!
(
node
instanceof
HTMLElement
))
{
continue
;
}
const
query
=
`[
${
ATTRIBUTE_FORM_DATASOURCE_ACTION
}
=write]`
;
if
(
this
.
getOption
(
"
features.writeBack
"
)
===
true
)
{
const
controls
=
node
.
querySelectorAll
(
query
);
const
events
=
this
.
getOption
(
"
writeBack.events
"
);
for
(
const
event
of
events
)
{
const
list
=
new
Set
([...
controls
]);
this
.
addEventListener
(
event
,
(
e
)
=>
{
if
(
node
.
matches
(
query
))
{
if
(
!
this
.
reportValidity
())
{
list
.
add
(
node
);
}
if
(
list
.
size
===
0
)
{
continue
;
}
initWriteActions
.
call
(
this
,
list
);
}
}
}
/**
* @private
* @param elements
*/
function
initWriteActions
(
elements
)
{
elements
.
forEach
((
element
)
=>
{
if
(
element
instanceof
HTMLElement
)
{
element
.
addEventListener
(
"
click
"
,
()
=>
{
runWriteCallback
.
call
(
this
,
element
);
});
const
g
=
element
?.
getOption
;
this
.
classList
.
add
(
"
invalid
"
);
if
(
!
isFunction
(
g
))
{
setTimeout
(()
=>
{
return
;
this
.
classList
.
remove
(
"
invalid
"
);
}
},
1000
)
const
s
=
element
?.
setOption
;
if
(
!
isFunction
(
s
))
{
return
;
}
const
fn
=
element
.
getOption
(
"
actions.click
"
);
if
(
!
isFunction
(
fn
))
{
return
;
return
;
}
}
// disable console.log of standard click event
if
(
this
[
debounceCallbackSymbol
]
instanceof
DeadMansSwitch
)
{
element
.
setOption
(
"
actions.click
"
,
function
()
{
// do nothing!
});
}
});
}
function
runWriteCallback
(
button
)
{
if
(
typeof
this
.
reportValidity
===
"
function
"
)
{
if
(
this
.
reportValidity
()
===
false
)
{
if
(
button
instanceof
StateButton
||
button
instanceof
MessageStateButton
)
{
button
.
setState
(
"
failed
"
);
}
return
;
}
}
const
datasource
=
this
.
getOption
(
"
datasource
"
);
if
(
!
(
datasource
instanceof
Datasource
))
{
return
;
}
if
(
button
instanceof
StateButton
||
button
instanceof
MessageStateButton
)
{
button
.
setState
(
"
activity
"
);
}
//const data = form?.[formDataSymbol]?.getRealSubject();
const
writePromise
=
datasource
.
set
(
this
[
formDataSymbol
].
getRealSubject
())
.
write
();
if
(
!
(
writePromise
instanceof
Promise
))
{
throw
new
Error
(
"
datasource.write() must return a promise
"
);
}
writePromise
.
then
((
r
)
=>
{
if
(
button
instanceof
StateButton
||
button
instanceof
MessageStateButton
)
{
button
.
setState
(
"
successful
"
);
}
this
[
formDataSymbol
].
getSubject
()[
PROPERTY_VALIDATION_KEY
]
=
{};
})
.
catch
((
e
)
=>
{
if
(
e
instanceof
WriteError
)
{
this
[
formDataSymbol
].
getSubject
()[
PROPERTY_VALIDATION_KEY
]
=
e
.
getValidation
();
}
if
(
button
instanceof
StateButton
||
button
instanceof
MessageStateButton
)
{
button
.
setState
(
"
failed
"
);
}
if
(
button
instanceof
MessageStateButton
)
{
button
.
setMessage
(
e
.
message
);
button
.
showMessage
();
}
});
}
/**
* This attribute can be used to pass a URL to this select.
*
* ```
* <monster-form data-monster-datasource="restapi:....."></monster-form>
* ```
*
* @private
* @return {object}
*/
function
initOptionsFromArguments
()
{
const
options
=
{};
const
datasource
=
this
.
getAttribute
(
ATTRIBUTE_FORM_DATASOURCE
);
if
(
isString
(
datasource
))
{
for
(
const
[
key
,
classObject
]
of
Form
.
getDatasources
())
{
if
(
datasource
===
key
)
{
let
args
=
this
.
getAttribute
(
ATTRIBUTE_FORM_DATASOURCE_ARGUMENTS
);
try
{
try
{
args
=
JSON
.
parse
(
args
);
this
[
debounceCallbackSymbol
].
touch
();
return
;
}
catch
(
e
)
{
}
catch
(
e
)
{
this
.
setAttribute
(
ATTRIBUTE_ERRORMESSAGE
,
e
.
toString
());
if
(
e
.
message
!==
"
has already run
"
)
{
continu
e
;
throw
e
;
}
}
delete
this
[
debounceCallbackSymbol
];
try
{
options
[
"
datasource
"
]
=
new
classObject
(
args
);
}
catch
(
e
)
{
this
.
setAttribute
(
ATTRIBUTE_ERRORMESSAGE
,
e
.
toString
());
continue
;
}
}
break
;
}
}
if
(
options
[
"
datas
ou
r
ce
"
]
instanceof
Datasource
)
{
this
[
deb
ou
n
ce
CallbackSymbol
]
=
new
DeadMansSwitch
(
200
,
()
=>
{
break
;
setTimeout
(()
=>
{
}
this
.
write
();
}
},
0
);
}
});
return
options
;
})
;
}
}
/**
* @private
* @this Form
*/
function
initObserver
()
{
const
self
=
this
;
let
lastDatasource
=
null
;
self
[
internalSymbol
].
attachObserver
(
new
Observer
(
function
()
{
const
datasource
=
self
.
getOption
(
"
datasource
"
);
if
(
datasource
!==
lastDatasource
)
{
new
Processing
(
100
,
function
()
{
self
.
refresh
();
}).
run
();
}
}
lastDatasource
=
datasource
;
return
this
;
}),
);
}
}
/**
/**
* @private
* @private
* @return {
Monster.Components.Form.Form
}
* @return {
FilterButton
}
*/
*/
function
initControlReferences
()
{
function
initControlReferences
()
{
if
(
!
this
.
shadowRoot
)
{
if
(
!
this
.
shadowRoot
)
{
throw
new
Error
(
"
no shadow-root is defined
"
);
throw
new
Error
(
"
no shadow-root is defined
"
);
}
}
this
[
formElementSymbol
]
=
this
.
shadowRoot
.
querySelector
(
"
[data-monster-role=form]
"
,
);
return
this
;
return
this
;
}
}
...
...
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment