mirror of
https://github.com/discourse/discourse.git
synced 2024-12-20 14:13:44 +08:00
DEV: adds support for nested collections and objects (#30265)
Collections were an existing concept in FormKit but didn't allow nesting. You can now do infinite nesting: ```gjs <Form @data={{hash foo=(array (hash bar=(array (hash baz=1))) (hash bar=(array (hash baz=2))) ) }} as |form| > <form.Collection @name="foo" as |parent parentIndex|> <parent.Collection @name="bar" as |child childIndex|> <child.Field @name="baz" @title="Baz" as |field|> <field.Input /> </child.Field> </parent.Collection> </form.Collection> </Form> ``` On top of this a new component has been added: `Object`. It allows you to represent objects in your form data. Collections are basically handling arrays, and Objects are objects. This is useful if you form data has this shape for example: ```javascript { foo: { bar: 1, baz: 2 } } ``` This can now be mapped in your form using this syntax: ```gjs <Form @data={{hash foo=(hash bar=1 baz=2)}} as |form|> <form.Object @name="foo" as |object name|> <object.Field @name={{name}} @title={{name}} as |field|> <field.Input /> </object.Field> </form.Object> </Form> ``` Objects accept nested collections and nested objects. Just like Collections. A small addition has also been made to `Collection`, they now support a custom `@tagName`, it's useful if each item of your collection is the row of a table for example.
This commit is contained in:
parent
b191d63c1b
commit
f6a4de4805
|
@ -1,40 +1,78 @@
|
||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { hash } from "@ember/helper";
|
import { concat, hash } from "@ember/helper";
|
||||||
import { action } from "@ember/object";
|
import { action, get } from "@ember/object";
|
||||||
import FKField from "discourse/form-kit/components/fk/field";
|
import FKField from "discourse/form-kit/components/fk/field";
|
||||||
|
import FKObject from "discourse/form-kit/components/fk/object";
|
||||||
|
import element from "discourse/helpers/element";
|
||||||
|
|
||||||
export default class FKCollection extends Component {
|
export default class FKCollection extends Component {
|
||||||
@action
|
@action
|
||||||
remove(index) {
|
remove(index) {
|
||||||
this.args.remove(this.args.name, index);
|
this.args.remove(this.name, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
get collectionValue() {
|
get collectionData() {
|
||||||
return this.args.data.get(this.args.name);
|
return this.args.data.get(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.args.parentName
|
||||||
|
? `${this.args.parentName}.${this.args.name}`
|
||||||
|
: this.args.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get tagName() {
|
||||||
|
return this.args.tagName || "div";
|
||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="form-kit__collection">
|
{{#let (element this.tagName) as |Wrapper|}}
|
||||||
{{#each this.collectionValue key="index" as |data index|}}
|
{{#each this.collectionData key="index" as |data index|}}
|
||||||
{{yield
|
<Wrapper class="form-kit__collection">
|
||||||
(hash
|
{{yield
|
||||||
Field=(component
|
(hash
|
||||||
FKField
|
Field=(component
|
||||||
errors=@errors
|
FKField
|
||||||
collectionName=@name
|
errors=@errors
|
||||||
collectionIndex=index
|
collectionIndex=index
|
||||||
addError=@addError
|
addError=@addError
|
||||||
data=@data
|
data=@data
|
||||||
set=@set
|
set=@set
|
||||||
registerField=@registerField
|
registerField=@registerField
|
||||||
unregisterField=@unregisterField
|
unregisterField=@unregisterField
|
||||||
triggerRevalidationFor=@triggerRevalidationFor
|
triggerRevalidationFor=@triggerRevalidationFor
|
||||||
|
parentName=(concat this.name "." index)
|
||||||
|
)
|
||||||
|
Object=(component
|
||||||
|
FKObject
|
||||||
|
errors=@errors
|
||||||
|
addError=@addError
|
||||||
|
data=@data
|
||||||
|
set=@set
|
||||||
|
registerField=@registerField
|
||||||
|
unregisterField=@unregisterField
|
||||||
|
triggerRevalidationFor=@triggerRevalidationFor
|
||||||
|
parentName=(concat this.name "." index)
|
||||||
|
)
|
||||||
|
Collection=(component
|
||||||
|
FKCollection
|
||||||
|
errors=@errors
|
||||||
|
addError=@addError
|
||||||
|
data=@data
|
||||||
|
set=@set
|
||||||
|
registerField=@registerField
|
||||||
|
unregisterField=@unregisterField
|
||||||
|
triggerRevalidationFor=@triggerRevalidationFor
|
||||||
|
parentName=(concat this.name "." index)
|
||||||
|
remove=@remove
|
||||||
|
)
|
||||||
|
remove=this.remove
|
||||||
)
|
)
|
||||||
remove=this.remove
|
index
|
||||||
)
|
(get this.collectionData index)
|
||||||
index
|
}}
|
||||||
}}
|
</Wrapper>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
{{/let}}
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,13 +146,11 @@ export default class FKFieldData extends Component {
|
||||||
throw new Error("@name can't include `.` or `-`.");
|
throw new Error("@name can't include `.` or `-`.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (this.args.parentName) {
|
||||||
(this.args.collectionName ? `${this.args.collectionName}.` : "") +
|
return `${this.args.parentName}.${this.args.name}`;
|
||||||
(this.args.collectionIndex !== undefined
|
}
|
||||||
? `${this.args.collectionIndex}.`
|
|
||||||
: "") +
|
return this.args.name;
|
||||||
this.args.name
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -57,7 +57,7 @@ export default class FKField extends Component {
|
||||||
@registerField={{@registerField}}
|
@registerField={{@registerField}}
|
||||||
@format={{@format}}
|
@format={{@format}}
|
||||||
@disabled={{@disabled}}
|
@disabled={{@disabled}}
|
||||||
@collectionName={{@collectionName}}
|
@parentName={{@parentName}}
|
||||||
as |field|
|
as |field|
|
||||||
>
|
>
|
||||||
<this.wrapper @size={{@size}}>
|
<this.wrapper @size={{@size}}>
|
||||||
|
|
|
@ -14,6 +14,7 @@ import FKErrorsSummary from "discourse/form-kit/components/fk/errors-summary";
|
||||||
import FKField from "discourse/form-kit/components/fk/field";
|
import FKField from "discourse/form-kit/components/fk/field";
|
||||||
import FKFieldset from "discourse/form-kit/components/fk/fieldset";
|
import FKFieldset from "discourse/form-kit/components/fk/fieldset";
|
||||||
import FKInputGroup from "discourse/form-kit/components/fk/input-group";
|
import FKInputGroup from "discourse/form-kit/components/fk/input-group";
|
||||||
|
import FKObject from "discourse/form-kit/components/fk/object";
|
||||||
import Row from "discourse/form-kit/components/fk/row";
|
import Row from "discourse/form-kit/components/fk/row";
|
||||||
import FKSection from "discourse/form-kit/components/fk/section";
|
import FKSection from "discourse/form-kit/components/fk/section";
|
||||||
import FKSubmit from "discourse/form-kit/components/fk/submit";
|
import FKSubmit from "discourse/form-kit/components/fk/submit";
|
||||||
|
@ -289,6 +290,17 @@ class FKForm extends Component {
|
||||||
unregisterField=this.unregisterField
|
unregisterField=this.unregisterField
|
||||||
triggerRevalidationFor=this.triggerRevalidationFor
|
triggerRevalidationFor=this.triggerRevalidationFor
|
||||||
)
|
)
|
||||||
|
Object=(component
|
||||||
|
FKObject
|
||||||
|
errors=this.formData.errors
|
||||||
|
addError=this.addError
|
||||||
|
data=this.formData
|
||||||
|
set=this.set
|
||||||
|
registerField=this.registerField
|
||||||
|
unregisterField=this.unregisterField
|
||||||
|
triggerRevalidationFor=this.triggerRevalidationFor
|
||||||
|
remove=this.remove
|
||||||
|
)
|
||||||
InputGroup=(component
|
InputGroup=(component
|
||||||
FKInputGroup
|
FKInputGroup
|
||||||
errors=this.formData.errors
|
errors=this.formData.errors
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { hash } from "@ember/helper";
|
||||||
|
import FKCollection from "discourse/form-kit/components/fk/collection";
|
||||||
|
import FKField from "discourse/form-kit/components/fk/field";
|
||||||
|
|
||||||
|
export default class FKObject extends Component {
|
||||||
|
get objectData() {
|
||||||
|
return this.args.data.get(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.args.parentName
|
||||||
|
? `${this.args.parentName}.${this.args.name}`
|
||||||
|
: this.args.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get keys() {
|
||||||
|
return Object.keys(this.objectData);
|
||||||
|
}
|
||||||
|
|
||||||
|
entryData(name) {
|
||||||
|
return this.objectData[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="form-kit__object">
|
||||||
|
{{#each this.keys key="index" as |name|}}
|
||||||
|
{{yield
|
||||||
|
(hash
|
||||||
|
Field=(component
|
||||||
|
FKField
|
||||||
|
errors=@errors
|
||||||
|
addError=@addError
|
||||||
|
data=@data
|
||||||
|
set=@set
|
||||||
|
registerField=@registerField
|
||||||
|
unregisterField=@unregisterField
|
||||||
|
triggerRevalidationFor=@triggerRevalidationFor
|
||||||
|
parentName=this.name
|
||||||
|
)
|
||||||
|
Object=(component
|
||||||
|
FKObject
|
||||||
|
errors=@errors
|
||||||
|
addError=@addError
|
||||||
|
data=@data
|
||||||
|
set=@set
|
||||||
|
registerField=@registerField
|
||||||
|
unregisterField=@unregisterField
|
||||||
|
triggerRevalidationFor=@triggerRevalidationFor
|
||||||
|
parentName=this.name
|
||||||
|
)
|
||||||
|
Collection=(component
|
||||||
|
FKCollection
|
||||||
|
errors=@errors
|
||||||
|
addError=@addError
|
||||||
|
data=@data
|
||||||
|
set=@set
|
||||||
|
registerField=@registerField
|
||||||
|
unregisterField=@unregisterField
|
||||||
|
triggerRevalidationFor=@triggerRevalidationFor
|
||||||
|
parentName=this.name
|
||||||
|
remove=@remove
|
||||||
|
)
|
||||||
|
)
|
||||||
|
name
|
||||||
|
(this.entryData name)
|
||||||
|
}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -3,11 +3,22 @@ import { click, render } from "@ember/test-helpers";
|
||||||
import { module, test } from "qunit";
|
import { module, test } from "qunit";
|
||||||
import Form from "discourse/components/form";
|
import Form from "discourse/components/form";
|
||||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||||
|
import formKit from "discourse/tests/helpers/form-kit-helper";
|
||||||
|
|
||||||
module("Integration | Component | FormKit | Collection", function (hooks) {
|
module("Integration | Component | FormKit | Collection", function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
test("default", async function (assert) {
|
test("@tagName", async function (assert) {
|
||||||
|
await render(<template>
|
||||||
|
<Form @data={{hash foo=(array (hash bar=1) (hash bar=2))}} as |form|>
|
||||||
|
<form.Collection @name="foo" @tagName="tr" />
|
||||||
|
</Form>
|
||||||
|
</template>);
|
||||||
|
|
||||||
|
assert.dom("tr.form-kit__collection").exists();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("field", async function (assert) {
|
||||||
await render(<template>
|
await render(<template>
|
||||||
<Form @data={{hash foo=(array (hash bar=1) (hash bar=2))}} as |form|>
|
<Form @data={{hash foo=(array (hash bar=1) (hash bar=2))}} as |form|>
|
||||||
<form.Collection @name="foo" as |collection|>
|
<form.Collection @name="foo" as |collection|>
|
||||||
|
@ -45,4 +56,85 @@ module("Integration | Component | FormKit | Collection", function (hooks) {
|
||||||
assert.form().field("foo.0.bar").hasValue("1");
|
assert.form().field("foo.0.bar").hasValue("1");
|
||||||
assert.form().field("foo.1.bar").doesNotExist();
|
assert.form().field("foo.1.bar").doesNotExist();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("nested object", async function (assert) {
|
||||||
|
await render(<template>
|
||||||
|
<Form
|
||||||
|
@data={{hash
|
||||||
|
foo=(array (hash bar=(hash baz=1)) (hash bar=(hash baz=2)))
|
||||||
|
}}
|
||||||
|
as |form|
|
||||||
|
>
|
||||||
|
<form.Collection @name="foo" as |collection index|>
|
||||||
|
<collection.Object @name="bar" as |object|>
|
||||||
|
<object.Field @name="baz" @title="Baz" as |field|>
|
||||||
|
<field.Input />
|
||||||
|
<form.Button
|
||||||
|
class={{concat "remove-" index}}
|
||||||
|
@action={{fn collection.remove index}}
|
||||||
|
>Remove</form.Button>
|
||||||
|
</object.Field>
|
||||||
|
</collection.Object>
|
||||||
|
</form.Collection>
|
||||||
|
</Form>
|
||||||
|
</template>);
|
||||||
|
|
||||||
|
assert.form().field("foo.0.bar.baz").hasValue("1");
|
||||||
|
assert.form().field("foo.1.bar.baz").hasValue("2");
|
||||||
|
|
||||||
|
await click(".remove-1");
|
||||||
|
|
||||||
|
assert.form().field("foo.0.bar.baz").hasValue("1");
|
||||||
|
assert.form().field("foo.1.bar.baz").doesNotExist();
|
||||||
|
|
||||||
|
await formKit().field("foo.0.bar.baz").fillIn("2");
|
||||||
|
|
||||||
|
assert.form().field("foo.0.bar.baz").hasValue("2");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("nested collection", async function (assert) {
|
||||||
|
await render(<template>
|
||||||
|
<Form
|
||||||
|
@data={{hash
|
||||||
|
one=(array
|
||||||
|
(hash two=(array (hash three=(array (hash foo=1) (hash foo=2)))))
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
as |form|
|
||||||
|
>
|
||||||
|
<form.Collection @name="one" as |first firstIndex|>
|
||||||
|
<first.Collection @name="two" as |second secondIndex|>
|
||||||
|
<second.Collection @name="three" as |third thirdIndex|>
|
||||||
|
<third.Field @name="foo" @title="Foo" as |field|>
|
||||||
|
<field.Input />
|
||||||
|
<form.Button
|
||||||
|
class={{concat
|
||||||
|
"remove-"
|
||||||
|
firstIndex
|
||||||
|
"-"
|
||||||
|
secondIndex
|
||||||
|
"-"
|
||||||
|
thirdIndex
|
||||||
|
}}
|
||||||
|
@action={{fn third.remove thirdIndex}}
|
||||||
|
>Remove</form.Button>
|
||||||
|
</third.Field>
|
||||||
|
</second.Collection>
|
||||||
|
</first.Collection>
|
||||||
|
</form.Collection>
|
||||||
|
</Form>
|
||||||
|
</template>);
|
||||||
|
|
||||||
|
assert.form().field("one.0.two.0.three.0.foo").hasValue("1");
|
||||||
|
assert.form().field("one.0.two.0.three.1.foo").hasValue("2");
|
||||||
|
|
||||||
|
await click(".remove-0-0-1");
|
||||||
|
|
||||||
|
assert.form().field("one.0.two.0.three.0.foo").hasValue("1");
|
||||||
|
assert.form().field("one.0.two.0.three.1.foo").doesNotExist();
|
||||||
|
|
||||||
|
await formKit().field("one.0.two.0.three.0.foo").fillIn("2");
|
||||||
|
|
||||||
|
assert.form().field("one.0.two.0.three.0.foo").hasValue("2");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { array, concat, fn, hash } from "@ember/helper";
|
||||||
|
import { click, render } from "@ember/test-helpers";
|
||||||
|
import { module, test } from "qunit";
|
||||||
|
import Form from "discourse/components/form";
|
||||||
|
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||||
|
import formKit from "discourse/tests/helpers/form-kit-helper";
|
||||||
|
|
||||||
|
module("Integration | Component | FormKit | Object", function (hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
test("field", async function (assert) {
|
||||||
|
await render(<template>
|
||||||
|
<Form @data={{hash foo=(hash bar=1 baz=2)}} as |form|>
|
||||||
|
<form.Object @name="foo" as |object name|>
|
||||||
|
<object.Field @name={{name}} @title={{name}} as |field|>
|
||||||
|
<field.Input />
|
||||||
|
</object.Field>
|
||||||
|
</form.Object>
|
||||||
|
</Form>
|
||||||
|
</template>);
|
||||||
|
|
||||||
|
assert.form().field("foo.bar").hasValue("1");
|
||||||
|
assert.form().field("foo.baz").hasValue("2");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("nested object", async function (assert) {
|
||||||
|
await render(<template>
|
||||||
|
<Form
|
||||||
|
@data={{hash one=(hash two=(hash three=(hash foo=1 bar=2)))}}
|
||||||
|
as |form|
|
||||||
|
>
|
||||||
|
<form.Object @name="one" as |one|>
|
||||||
|
<one.Object @name="two" as |two|>
|
||||||
|
<two.Object @name="three" as |three name|>
|
||||||
|
<three.Field @name={{name}} @title={{name}} as |field|>
|
||||||
|
<field.Input />
|
||||||
|
</three.Field>
|
||||||
|
</two.Object>
|
||||||
|
</one.Object>
|
||||||
|
</form.Object>
|
||||||
|
</Form>
|
||||||
|
</template>);
|
||||||
|
|
||||||
|
assert.form().field("one.two.three.foo").hasValue("1");
|
||||||
|
assert.form().field("one.two.three.bar").hasValue("2");
|
||||||
|
|
||||||
|
await formKit().field("one.two.three.foo").fillIn("2");
|
||||||
|
|
||||||
|
assert.form().field("one.two.three.foo").hasValue("2");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("nested collection", async function (assert) {
|
||||||
|
await render(<template>
|
||||||
|
<Form
|
||||||
|
@data={{hash one=(hash two=(array (hash foo=1) (hash foo=2)))}}
|
||||||
|
as |form|
|
||||||
|
>
|
||||||
|
<form.Object @name="one" as |one|>
|
||||||
|
<one.Collection @name="two" as |two twoIndex|>
|
||||||
|
<two.Field @name="foo" @title="foo" as |field|>
|
||||||
|
<field.Input />
|
||||||
|
</two.Field>
|
||||||
|
<form.Button
|
||||||
|
class={{concat "remove-" twoIndex}}
|
||||||
|
@action={{fn two.remove twoIndex}}
|
||||||
|
>Remove</form.Button>
|
||||||
|
</one.Collection>
|
||||||
|
</form.Object>
|
||||||
|
</Form>
|
||||||
|
</template>);
|
||||||
|
|
||||||
|
assert.form().field("one.two.0.foo").hasValue("1");
|
||||||
|
assert.form().field("one.two.1.foo").hasValue("2");
|
||||||
|
|
||||||
|
await click(".remove-1");
|
||||||
|
|
||||||
|
assert.form().field("one.two.0.foo").hasValue("1");
|
||||||
|
assert.form().field("one.two.1.foo").doesNotExist();
|
||||||
|
|
||||||
|
await formKit().field("one.two.0.foo").fillIn("2");
|
||||||
|
|
||||||
|
assert.form().field("one.two.0.foo").hasValue("2");
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,5 +1,7 @@
|
||||||
.form-kit__collection {
|
.form-kit__collection {
|
||||||
display: flex;
|
&div {
|
||||||
flex-direction: column;
|
display: flex;
|
||||||
gap: var(--form-kit-gutter-y);
|
flex-direction: column;
|
||||||
|
gap: var(--form-kit-gutter-y);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
@import "_alert";
|
@import "_alert";
|
||||||
@import "_char-counter";
|
@import "_char-counter";
|
||||||
@import "_col";
|
@import "_col";
|
||||||
|
@import "_object";
|
||||||
@import "_collection";
|
@import "_collection";
|
||||||
@import "_conditional-display";
|
@import "_conditional-display";
|
||||||
@import "_container";
|
@import "_container";
|
||||||
|
|
3
app/assets/stylesheets/common/form-kit/_object.scss
Normal file
3
app/assets/stylesheets/common/form-kit/_object.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.form-kit__object {
|
||||||
|
display: contents;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user