Skip to content

Commit

Permalink
Merge pull request #40 from xenit-eu/hal-forms-manager
Browse files Browse the repository at this point in the history
HAL-FORMS value manager and request encoder
  • Loading branch information
vierbergenlars authored Mar 25, 2024
2 parents 213fde6 + 49d5e2c commit e516017
Show file tree
Hide file tree
Showing 22 changed files with 1,175 additions and 47 deletions.
2 changes: 1 addition & 1 deletion config/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
/* Modules */
"module": "ES6", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
"moduleResolution": "node16", /* Specify how TypeScript looks up a file from a given module specifier. */
"moduleResolution": "Bundler", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
Expand Down
58 changes: 58 additions & 0 deletions packages/hal-forms/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,64 @@ template.properties.forEach(property => {

</details>

### Interactive form value management

When you have a form, you probably want to display it to your users in some way so they can enter values in your form fields.

The `@contentgrid/hal-forms/values` package is a simple utility to manage the form field values entered by users.

It uses the correct datatype belonging to each form field.

<details>

<summary>Code example for form value management using HAL-FORMS values</summary>

```typescript
import { createValues } from "@contentgrid/hal-forms/values"

const templateValues = createValues(template); // template is a HalFormsTemplate

templateValues.values.forEach(value => {
console.log(`Field ${value.property.name}: ${value.value}`)
})

templateValues = templateValues.withValue("name", "Jeff"); // Creates a new object with the value set

templateValues.values.forEach(value => {
console.log(`Field ${value.property.name}: ${value.value}`)
})
```

</details>

### Encoding HAL-FORMS into requests

After collecting user input, you probably want to send a request to the backend to submit the values entered in the form.

The `@contentgrid/hal-forms/codecs` sub-package encodes the values according to the content-type required by the HAL-FORMS template.

<details>

<summary>Code example for encoding form values with codecs</summary>

```typescript
import codecs from "@contentgrid/hal-forms/codecs";

const request = codecs.requireCodecFor(template) // template is a HalFormsTemplate
.encode(templateValues); // templateValues is a HalFormValues

// The HAL-FORMS template method, target, contentType and values are encoded in request

// Put some additional headers on your request here
request.headers.set('Authorization', 'Basic ....');

// Perform the HTTP request
const response = await fetch(request);
console.log(response);
```

</details>

### Shapes

The `@contentgrid/hal-forms/shape` sub-package provides POJO (plain old javascript object) types that can be used to represent the raw HAL-FORMS JSON data.
Expand Down
136 changes: 136 additions & 0 deletions packages/hal-forms/__tests__/codecs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { describe, expect, test } from "@jest/globals";
import buildTemplate from "../src/builder";
import { default as codecs, Encoders, HalFormsCodecNotAvailableError, HalFormsCodecs } from "../src/codecs";
import { createValues } from "../src/values";

const form = buildTemplate("POST", "http://localhost/invoices")
.withContentType("application/json")
.addProperty("created_at", b => b
.withType("datetime")
.withRequired(true)
)
.addProperty("total.net", b => b
.withType("number")
)
.addProperty("total.vat", b => b
.withType("number")
)
.addProperty("senders", b => b.withType("url")
.withOptions(b => b.withMinItems(0))
)
.addProperty("name", b => b.withValue("Jefke"));

describe("HalFormsCodecs", () => {

describe("#findCodecFor()", () => {
test("finds a codec", () => {
const c = codecs.findCodecFor(form);
expect(c).not.toBeNull();
})

test("does not find a codec", () => {
const empty = HalFormsCodecs.builder()
.registerEncoder("example/json", Encoders.json())
.build();
const c = empty.findCodecFor(form);
expect(c).toBeNull();
})
});

describe("#requireCodecFor()", () => {

test("requires a codec", () => {
const c = codecs.requireCodecFor(form);
expect(c).not.toBeNull();
})

test("does not find a required codec", () => {
const empty = HalFormsCodecs.builder().build();
expect(() => empty.requireCodecFor(form))
.toThrowError(new HalFormsCodecNotAvailableError(form));
})

})
})

describe("Encoders.json()", () => {
const values = createValues(form);
const codecs = HalFormsCodecs.builder()
.registerEncoder("application/json", Encoders.json())
.build();

test("Encodes default values", () => {
const encoded = codecs.requireCodecFor(form).encode(values.values);

expect(encoded).toBeInstanceOf(Request);

expect(encoded.json())
.resolves
.toEqual({
"name": "Jefke"
});
})

test("Encodes nested values as flat JSON", () => {

const encoded = codecs.requireCodecFor(form).encode(
values
.withValue("total.net", 123)
.withValue("total.vat", 456)
.values
);

expect(encoded).toBeInstanceOf(Request);

expect(encoded.json())
.resolves
.toEqual({
"name": "Jefke",
"total.net": 123,
"total.vat": 456
})
});

})

describe("Encoders.nestedJson()", () => {
const values = createValues(form);
const codecs = HalFormsCodecs.builder()
.registerEncoder("application/json", Encoders.nestedJson())
.build();

test("Encodes default values", () => {
const encoded = codecs.requireCodecFor(form).encode(values.values);

expect(encoded).toBeInstanceOf(Request);

expect(encoded.json())
.resolves
.toEqual({
"name": "Jefke"
});
})

test("Encodes nested values as flat JSON", () => {

const encoded = codecs.requireCodecFor(form).encode(
values
.withValue("total.net", 123)
.withValue("total.vat", 456)
.values
);

expect(encoded).toBeInstanceOf(Request);

expect(encoded.json())
.resolves
.toEqual({
"name": "Jefke",
"total": {
"net": 123,
"vat": 456
}
})
});

})
18 changes: 12 additions & 6 deletions packages/hal-forms/__tests__/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe("resolveTemplate", () => {
_templates: {
["create-form"]: {
method: "GET",
title: "Create new",
target: "http://localhost/create",
properties: [
{
Expand Down Expand Up @@ -56,6 +57,7 @@ describe("resolveTemplate", () => {
},
other: {
method: "POST",
contentType: "application/x-www-form-urlencoded",
properties: [
{
name: "ZZZ",
Expand All @@ -73,13 +75,15 @@ describe("resolveTemplate", () => {
method: "GET",
url: "http://localhost/create"
})
expect(template?.contentType).toEqual("application/json");
expect(template?.title).toEqual("Create new");
expect(template?.properties.length).toEqual(3);
const propAbc = template!.property("abc");
expect(template?.property("abc").readOnly).toBe(false);
expect(template?.property("abc").type).toEqual("text");
const abcOpts = propAbc.options;
expect(abcOpts.isInline()).toBe(true);
expect(abcOpts.loadOptions(() => { throw new Error("Not implemented") }))
expect(abcOpts!.isInline()).toBe(true);
expect(abcOpts!.loadOptions(() => { throw new Error("Not implemented") }))
.resolves
.toEqual([
{
Expand All @@ -92,18 +96,18 @@ describe("resolveTemplate", () => {
}
])

if(abcOpts.isInline()) {
if(abcOpts!.isInline()) {
expect(abcOpts.inline.length).toBe(2);
}

expect(template?.property("def").readOnly).toBe(false);
expect(template?.property("def").type).toBe("number");
const defOpts = template!.property("def").options;
expect(defOpts.isRemote()).toBe(true);
expect(defOpts!.isRemote()).toBe(true);

const mockLoad = jest.fn(() => Promise.resolve(["1", "2", "3"]));

expect(defOpts.loadOptions(mockLoad))
expect(defOpts!.loadOptions(mockLoad))
.resolves
.toEqual([
{
Expand All @@ -120,7 +124,7 @@ describe("resolveTemplate", () => {
}
]);

if(abcOpts.isRemote()) {
if(abcOpts!.isRemote()) {
expect(mockLoad.mock.lastCall).toHaveBeenCalledWith(abcOpts.link);
expect(abcOpts.link.href).toEqual("http://localhost/numbers?q=4");
}
Expand All @@ -135,6 +139,8 @@ describe("resolveTemplate", () => {
method: "POST",
url: "http://localhost/item/4"
})
expect(template?.title).toBeUndefined();
expect(template?.contentType).toEqual("application/x-www-form-urlencoded")
expect(template?.properties.length).toEqual(1);
expect(template?.property("ZZZ").required).toBe(true);
})
Expand Down
Loading

0 comments on commit e516017

Please sign in to comment.