diff --git a/README.md b/README.md index c271398..d75f520 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ - [Executing long running methods in JavaScript](#executing-long-running-methods-in-JavaScript) - [Single page application](#single-page-application) - [React component](#react-component) - - [Form](#form) + - [JSON schema powered form](#json-schema-powered-form) - [Visualization](#visualization) [![CI](https://github.com/NLESC-JCER/cpp2wasm/workflows/CI/badge.svg)](https://github.com/NLESC-JCER/cpp2wasm/actions?query=workflow%3ACI) @@ -157,13 +157,23 @@ An example of JSON schema: ```json { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://example.com/schemas/person.json", + "$id": "https://nlesc-jcer.github.io/cpp2wasm/NNRequest.json", "type": "object", "properties": { - "name": { "type": "string" }, - "age": { "type": "number", "minimum": 0 } + "epsilon": { + "title": "Epsilon", + "type": "number", + "minimum": 0 + }, + "guess": { + "title": "Initial guess", + "type": "integer", + "minimum": -100, + "maximum": 100 + } }, - "required": [ "name" ] + "required": ["epsilon", "guess"], + "additionalProperties": false } ``` @@ -171,8 +181,8 @@ And a valid document: ```json { - "name": "me", - "age": 42 + "epsilon": 0.001, + "guess": -20 } ``` @@ -920,8 +930,8 @@ To use React we need to import the React library. A React application is constructed from React components. The simplest React component is a function which returns a HTML tag with a variable inside. -```{.jsx file=src/js/app.js} -// this JavaScript snippet is stored as src/js/app.js +```{.jsx #heading-component} +// this JavaScript snippet is later referred to as <> function Heading() { const title = 'Root finding web application'; return

{title}

@@ -995,12 +1005,12 @@ const [epsilon, setEpsilon] = React.useState(0.001); The argument of the `useState` function is the initial value. The `epsilon` variable contains the current value for epsilon and `setEpsilon` is a function to set epsilon to a new value. -The input tag in the form will call the `onChange` function with a event object. We need to extract the user input from the event and pass it to `setEpsilon`. +The input tag in the form will call the `onChange` function with a event object. We need to extract the user input from the event and pass it to `setEpsilon`. The value should be a number, so we use `Number()` to cast the string from the event to a number. ```{.js #react-state} // this JavaScript snippet is appended to <> function onEpsilonChange(event) { - setEpsilon(event.target.value); + setEpsilon(Number(event.target.value)); } ``` @@ -1011,7 +1021,7 @@ We will follow the same steps for the guess input as well. const [guess, setGuess] = React.useState(-20); function onGuessChange(event) { - setGuess(event.target.value); + setGuess(Number(event.target.value)); } ``` @@ -1066,8 +1076,8 @@ To render the result we can use a React Component which has `root` as a property When the calculation has not been done yet, it will render `Not submitted`. When the `root` property value is set then we will show it. -```{.jsx file=src/js/app.js} -// this JavaScript snippet stored as src/js/app.js +```{.jsx #result-component} +// this JavaScript snippet is later referred to as <> function Result(props) { const root = props.root; let message = 'Not submitted'; @@ -1081,6 +1091,9 @@ function Result(props) { We can combine the heading, form and result components and all the states and handleSubmit function into the `App` React component. ```{.jsx file=src/js/app.js} +<> +<> + // this JavaScript snippet appenended to src/js/app.js function App() { <> @@ -1119,8 +1132,168 @@ Visit [http://localhost:8000/src/js/example-app.html](http://localhost:8000/src/ ### JSON schema powered form -The JSON schema can be used to generate a form. The form submission will be validated against the schema. -The most popular JSON schema form for React is [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form). +The JSON schema can be used to generate a form. The form values will be validated against the schema. +The most popular JSON schema form for React is [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form) so we will write a web application with it. + +In the [Web service](#web-service) an OpenAPI specification was used to specify the request and response schema. For the form we need the request schema in JSON format which is + +```{.js #jsonschema-app} +// this JavaScript snippet is later referred to as <> +const schema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://nlesc-jcer.github.io/cpp2wasm/NNRequest.json", + "type": "object", + "properties": { + "epsilon": { + "title": "Epsilon", + "type": "number", + "minimum": 0 + }, + "guess": { + "title": "Initial guess", + "type": "integer", + "minimum": -100, + "maximum": 100 + } + }, + "required": ["epsilon", "guess"], + "additionalProperties": false +} +``` + +To render the application we need a HTML page. We will reuse the imports we did in the previous chapter. + +```{.html file=src/js/example-jsonschema-form.html} + + + + <> +
+ + + +``` + +To use the [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form) React component we need to import it. + +```{.html #imports} + + +``` + +The form component is exported as `JSONSchemaForm.default` and can be aliases to `Form` with + +```{.js #jsonschema-app} +// this JavaScript snippet is appended to <> +const Form = JSONSchemaForm.default; +``` + +The form [by default](https://react-jsonschema-form.readthedocs.io/en/latest/usage/themes/) uses the [Bootstrap 3](https://getbootstrap.com/docs/3.4/) theme. The theme injects class names into the HTML tags. The styles associated with the class names must be imported from the Bootstrap CSS file. + +```{.html #imports} + + +``` + +The react-jsonschema-form component normally renders an integer with a updown selector. To use a range slider instead configure a [user interface schema](https://react-jsonschema-form.readthedocs.io/en/latest/quickstart/#form-uischema). + +```{.js #jsonschema-app} +const uiSchema = { + "guess": { + "ui:widget": "range" + } +} +``` + +The values in the form must be initialized and updated whenever the form changes. + +```{.js #jsonschema-app} +// this JavaScript snippet is appended to <> +const [formData, setFormData] = React.useState({ + epsilon: 0.001, + guess: -20 +}); + +function handleChange(event) { + setFormData(event.formData); +} +``` + +The form can be rendered with + +```{.jsx #jsonschema-form} +{ /* this JavaScript snippet is later referred to as <> */} +
+``` + +The `handleSubmit` function recieves the form input values and use the web worker we created earlier to perform the calculation and render the result. + +```{.js #jsonschema-app} +// this JavaScript snippet is appended to <> +const [root, setRoot] = React.useState(undefined); + +function handleSubmit({formData}, event) { + event.preventDefault(); + const worker = new Worker('worker.js'); + worker.postMessage({ + type: 'CALCULATE', + payload: formData + }); + worker.onmessage = function(message) { + if (message.data.type === 'RESULT') { + const result = message.data.payload.root; + setRoot(result); + worker.terminate(); + } + }; +} +``` + +The App component can be defined and rendered with. + +```{.jsx file=src/js/jsonschema-app.js} +// this JavaScript snippet stored as src/js/jsonschema-app.js +function App() { + <> + + return ( +
+ + <> + +
+ ); +} + +ReactDOM.render( + , + document.getElementById('container') +); +``` + +The `Heading` and `Result` React component can be reused. + +```{.jsx file=src/js/jsonschema-app.js} +// this JavaScript snippet appended to src/js/jsonschema-app.js +<> +<> +``` + +Like before we also need to host the files in a web server with + +```shell +python3 -m http.server 8000 +``` + +Visit [http://localhost:8000/src/js/example-jsonschema-form.html](http://localhost:8000/src/js/example-jsonschema-form.html) to see the root answer. + +If you enter a negative number in the `epsilon` field the form will become invalid with a error message. ### Visualization diff --git a/TESTING.md b/TESTING.md index a8fae55..bff8b23 100644 --- a/TESTING.md +++ b/TESTING.md @@ -32,12 +32,28 @@ describe('src/js/example-web-worker.html', () => { ``` And lastly, a test for the React/form/Web worker/WebAssembly combination. +Let us also change the guess value. ```{.js file=cypress/integration/example-app_spec.js} describe('src/js/example-app.html', () => { it('should render -1.00', () => { cy.visit('http://localhost:8000/src/js/example-app.html'); - cy.get('input[name=guess]').type('-30'); + cy.get('input[name=guess]').type('0'); + // TODO assert value is set + cy.contains('Submit').click(); + cy.get('#answer').contains('-1.00'); + }); +}); +``` + +And another test for the full application, but now with JSON schema powered form. + +```{.js file=cypress/integration/example-jsonschema-form_spec.js} +describe('src/js/example-jsonschema-form.html', () => { + it('should render -1.00', () => { + cy.visit('http://localhost:8000/src/js/example-jsonschema-form.html'); + cy.get('input[id=root_epsilon]').type('{selectall}0.1'); + // TODO assert value is set cy.contains('Submit').click(); cy.get('#answer').contains('-1.00'); }); diff --git a/cypress/integration/example-app_spec.js b/cypress/integration/example-app_spec.js index f831bac..65ea69e 100644 --- a/cypress/integration/example-app_spec.js +++ b/cypress/integration/example-app_spec.js @@ -1,7 +1,8 @@ describe('src/js/example-app.html', () => { it('should render -1.00', () => { cy.visit('http://localhost:8000/src/js/example-app.html'); - cy.get('input[name=guess]').type('-30'); + cy.get('input[name=guess]').type('0'); + // TODO assert value is set cy.contains('Submit').click(); cy.get('#answer').contains('-1.00'); }); diff --git a/cypress/integration/example-jsonschema-form_spec.js b/cypress/integration/example-jsonschema-form_spec.js new file mode 100644 index 0000000..ead379d --- /dev/null +++ b/cypress/integration/example-jsonschema-form_spec.js @@ -0,0 +1,9 @@ +describe('src/js/example-jsonschema-form.html', () => { + it('should render -1.00', () => { + cy.visit('http://localhost:8000/src/js/example-jsonschema-form.html'); + cy.get('input[id=root_epsilon]').type('{selectall}0.1'); + // TODO assert value is set + cy.contains('Submit').click(); + cy.get('#answer').contains('-1.00'); + }); +}); \ No newline at end of file diff --git a/src/js/app.js b/src/js/app.js index a320aff..239d078 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -1,9 +1,9 @@ -// this JavaScript snippet is stored as src/js/app.js +// this JavaScript snippet is later referred to as <> function Heading() { const title = 'Root finding web application'; return

{title}

} -// this JavaScript snippet stored as src/js/app.js +// this JavaScript snippet is later referred to as <> function Result(props) { const root = props.root; let message = 'Not submitted'; @@ -12,19 +12,20 @@ function Result(props) { } return
{message}
; } + // this JavaScript snippet appenended to src/js/app.js function App() { // this JavaScript snippet is later referred to as <> const [epsilon, setEpsilon] = React.useState(0.001); // this JavaScript snippet is appended to <> function onEpsilonChange(event) { - setEpsilon(event.target.value); + setEpsilon(Number(event.target.value)); } // this JavaScript snippet is appended to <> const [guess, setGuess] = React.useState(-20); function onGuessChange(event) { - setGuess(event.target.value); + setGuess(Number(event.target.value)); } // this JavaScript snippet is appended to <> const [root, setRoot] = React.useState(undefined); diff --git a/src/js/example-app.html b/src/js/example-app.html index e24de67..fab26d9 100644 --- a/src/js/example-app.html +++ b/src/js/example-app.html @@ -6,6 +6,10 @@ + + + +
diff --git a/src/js/example-jsonschema-form.html b/src/js/example-jsonschema-form.html new file mode 100644 index 0000000..d3fc452 --- /dev/null +++ b/src/js/example-jsonschema-form.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/src/js/jsonschema-app.js b/src/js/jsonschema-app.js new file mode 100644 index 0000000..0237341 --- /dev/null +++ b/src/js/jsonschema-app.js @@ -0,0 +1,93 @@ +// this JavaScript snippet stored as src/js/jsonschema-app.js +function App() { + // this JavaScript snippet is later referred to as <> + const schema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://nlesc-jcer.github.io/cpp2wasm/NNRequest.json", + "type": "object", + "properties": { + "epsilon": { + "title": "Epsilon", + "type": "number", + "minimum": 0 + }, + "guess": { + "title": "Initial guess", + "type": "integer", + "minimum": -100, + "maximum": 100 + } + }, + "required": ["epsilon", "guess"], + "additionalProperties": false + } + // this JavaScript snippet is appended to <> + const Form = JSONSchemaForm.default; + const uiSchema = { + "guess": { + "ui:widget": "range" + } + } + // this JavaScript snippet is appended to <> + const [formData, setFormData] = React.useState({ + epsilon: 0.001, + guess: -20 + }); + + function handleChange(event) { + setFormData(event.formData); + } + // this JavaScript snippet is appended to <> + const [root, setRoot] = React.useState(undefined); + + function handleSubmit({formData}, event) { + event.preventDefault(); + const worker = new Worker('worker.js'); + worker.postMessage({ + type: 'CALCULATE', + payload: formData + }); + worker.onmessage = function(message) { + if (message.data.type === 'RESULT') { + const result = message.data.payload.root; + setRoot(result); + worker.terminate(); + } + }; + } + + return ( +
+ + { /* this JavaScript snippet is later referred to as <> */} + + +
+ ); +} + +ReactDOM.render( + , + document.getElementById('container') +); +// this JavaScript snippet appended to src/js/jsonschema-app.js +// this JavaScript snippet is later referred to as <> +function Heading() { + const title = 'Root finding web application'; + return

{title}

+} +// this JavaScript snippet is later referred to as <> +function Result(props) { + const root = props.root; + let message = 'Not submitted'; + if (root !== undefined) { + message = 'Root = ' + root; + } + return
{message}
; +} \ No newline at end of file