diff --git a/packages/oui-angular/src/index.js b/packages/oui-angular/src/index.js index 2b5f6d77..c32f6756 100644 --- a/packages/oui-angular/src/index.js +++ b/packages/oui-angular/src/index.js @@ -14,6 +14,7 @@ import Field from "@ovh-ui/oui-field"; import FormActions from "@ovh-ui/oui-form-actions"; import GuideMenu from "@ovh-ui/oui-guide-menu"; import HeaderTabs from "@ovh-ui/oui-header-tabs"; +import InlineAdder from "@ovh-ui/oui-inline-adder"; import Message from "@ovh-ui/oui-message"; import Modal from "@ovh-ui/oui-modal"; import Navbar from "@ovh-ui/oui-navbar"; @@ -53,6 +54,7 @@ export default angular FormActions, GuideMenu, HeaderTabs, + InlineAdder, Message, Modal, Navbar, diff --git a/packages/oui-angular/src/index.spec.js b/packages/oui-angular/src/index.spec.js index ed8ea126..67b46a72 100644 --- a/packages/oui-angular/src/index.spec.js +++ b/packages/oui-angular/src/index.spec.js @@ -16,6 +16,7 @@ loadTests(require.context("../../oui-field/src/", true, /.*((\.spec)|(index))$/) loadTests(require.context("../../oui-form-actions/src/", true, /.*((\.spec)|(index))$/)); loadTests(require.context("../../oui-guide-menu/src/", true, /.*((\.spec)|(index))$/)); loadTests(require.context("../../oui-header-tabs/src/", true, /.*((\.spec)|(index))$/)); +loadTests(require.context("../../oui-inline-adder/src/", true, /.*((\.spec)|(index))$/)); loadTests(require.context("../../oui-message/src/", true, /.*((\.spec)|(index))$/)); loadTests(require.context("../../oui-modal/src/", true, /.*((\.spec)|(index))$/)); loadTests(require.context("../../oui-navbar/src/", true, /.*((\.spec)|(index))$/)); diff --git a/packages/oui-inline-adder/README.md b/packages/oui-inline-adder/README.md new file mode 100644 index 00000000..76292d0f --- /dev/null +++ b/packages/oui-inline-adder/README.md @@ -0,0 +1,168 @@ +# Inline adder + + + +## Usage + +### Basic + +```html:preview + + + + + + + + + + + + + + +``` + +### Multiple rows + +```html:preview + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### Adaptive fields + +**Note**: Fields with `adaptive` attribute will adapt their size to their content. + +```html:preview + + + + + + + + + + + + + + + + + + + +``` + +### Events + +#### `on-add` + +**Note**: If you want to access the form inside `on-add` callback, you need to use the `form` variable as below. + +```html:preview + + + + + + + + + +
+

On Add

+
{{$ctrl.addedForm | json}}
+
+``` + +#### `on-remove` + +**Note**: If you want to access the form inside `on-remove` callback, you need to use the `form` variable as below. + +```html:preview + + + + + + + + + +
+

On Remove

+
{{$ctrl.removedForm | json}}
+
+``` + +#### `on-change` + +**Note**: If you want to access the forms array inside `on-change` callback, you need to use the `forms` variable as below. + +```html:preview + + + + + + + + + +
+

On Change

+
{{$ctrl.changedForms | json}}
+
+``` + +## API + +### oui-inline-adder + +| Attribute | Type | Binding | One-time binding | Values | Default | Description +| ---- | ---- | ---- | ---- | ---- | ---- | ---- +| `on-add` | function | & | no | n/a | n/a | handler triggered when a row is added +| `on-remove` | function | & | no | n/a | n/a | handler triggered when a row is removed +| `on-change` | function | & | no | n/a | n/a | handler triggered when rows have changed + +### oui-inline-adder-field + +| Attribute | Type | Binding | One-time binding | Values | Default | Description +| ---- | ---- | ---- | ---- | ---- | ---- | ---- +| `adaptive` | boolean | { + this.$element.addClass("oui-inline-adder__field"); + + if (this.adaptive) { + this.$element.addClass("oui-inline-adder__field_adaptive"); + } + }); + } +} diff --git a/packages/oui-inline-adder/src/index.js b/packages/oui-inline-adder/src/index.js new file mode 100644 index 00000000..d2b791a2 --- /dev/null +++ b/packages/oui-inline-adder/src/index.js @@ -0,0 +1,12 @@ +import InlineAdder from "./inline-adder.component"; +import InlineAdderField from "./field/inline-adder-field.component"; +import InlineAdderProvider from "./inline-adder.provider"; +import InlineAdderRow from "./row/inline-adder-row.component"; + +export default angular + .module("oui.inline-adder", []) + .component("ouiInlineAdder", InlineAdder) + .component("ouiInlineAdderField", InlineAdderField) + .component("ouiInlineAdderRow", InlineAdderRow) + .provider("ouiInlineAdderConfiguration", InlineAdderProvider) + .name; diff --git a/packages/oui-inline-adder/src/index.spec.js b/packages/oui-inline-adder/src/index.spec.js new file mode 100644 index 00000000..f3a16ac3 --- /dev/null +++ b/packages/oui-inline-adder/src/index.spec.js @@ -0,0 +1,176 @@ +describe("ouiInlineAdder", () => { + let TestUtils; + let $timeout; + let configuration; + + beforeEach(angular.mock.module("oui.inline-adder")); + beforeEach(angular.mock.module("oui.inline-adder.configuration")); + beforeEach(angular.mock.module("oui.test-utils")); + + beforeEach(inject((_TestUtils_, _$timeout_) => { + TestUtils = _TestUtils_; + $timeout = _$timeout_; + })); + + describe("Provider", () => { + angular.module("oui.inline-adder.configuration", [ + "oui.inline-adder" + ]).config(ouiInlineAdderConfigurationProvider => { + ouiInlineAdderConfigurationProvider.setTranslations({ + foo: "bar" + }); + }); + + beforeEach(inject(_ouiInlineAdderConfiguration_ => { + configuration = _ouiInlineAdderConfiguration_; + })); + + it("should have custom options", () => { + expect(configuration.translations.foo).toEqual("bar"); + }); + }); + + describe("Component", () => { + describe("oui-inline-adder", () => { + it("should have a default classname", () => { + const element = TestUtils.compileTemplate(` + + + + + + + + + `); + + $timeout.flush(); + + expect(element.hasClass("oui-inline-adder")).toBeTruthy(); + }); + + it("should have a form with a default id/name", () => { + const element = TestUtils.compileTemplate(` + + + + + + + + + `); + + const form = element.find("form"); + const id = `${element.controller("ouiInlineAdder").id}_0`; + const name = `${element.controller("ouiInlineAdder").name}_0`; + + expect(form.length).toBe(1); + expect(form.attr("id")).toBe(id); + expect(form.attr("name")).toBe(name); + }); + + it("should create a new form when submitted", () => { + const element = TestUtils.compileTemplate(` + + + + + + + + + `); + + let length = element.find("form").length; + + // Will have 2 rows + angular.element(element.find("form")[length - 1]).triggerHandler("submit"); + expect(element.find("form").length).toBe(length += 1); + + // Will have 3 rows + angular.element(element.find("form")[length - 1]).triggerHandler("submit"); + expect(element.find("form").length).toBe(length += 1); + }); + + it("should hide a form when removed", () => { + const element = TestUtils.compileTemplate(` + + + + + + + + + `); + + let length = element.find("form").length; + const form = angular.element(element.find("form")[0]); + + // Will have 2 rows + form.triggerHandler("submit"); + expect(element.find("form").length).toBe(length += 1); + + // Will have 1 row hidden + form.find("button").triggerHandler("click"); + expect(form.hasClass("ng-hide")).toBeTruthy(); + }); + }); + + describe("oui-inline-adder-row", () => { + it("should have a default classname", () => { + const element = TestUtils.compileTemplate(` + + + + + + + + + `); + + $timeout.flush(); + + expect(element.find("oui-inline-adder-row").hasClass("oui-inline-adder__row")).toBeTruthy(); + }); + }); + + describe("oui-inline-adder-field", () => { + it("should have a default classname", () => { + const element = TestUtils.compileTemplate(` + + + + + + + + + `); + + $timeout.flush(); + + expect(element.find("oui-inline-adder-field").hasClass("oui-inline-adder__field")).toBeTruthy(); + }); + + it("should have a variant classname", () => { + const element = TestUtils.compileTemplate(` + + + + + + + + + `); + + $timeout.flush(); + + expect(element.find("oui-inline-adder-field").hasClass("oui-inline-adder__field_adaptive")).toBeTruthy(); + }); + }); + }); +}); diff --git a/packages/oui-inline-adder/src/inline-adder.component.js b/packages/oui-inline-adder/src/inline-adder.component.js new file mode 100644 index 00000000..0a60a478 --- /dev/null +++ b/packages/oui-inline-adder/src/inline-adder.component.js @@ -0,0 +1,15 @@ +import controller from "./inline-adder.controller.js"; +import template from "./inline-adder.html"; + +export default { + bindings: { + id: "@?", + name: "@?", + onAdd: "&", + onChange: "&", + onRemove: "&" + }, + controller, + template, + transclude: true +}; diff --git a/packages/oui-inline-adder/src/inline-adder.controller.js b/packages/oui-inline-adder/src/inline-adder.controller.js new file mode 100644 index 00000000..e8bf4b53 --- /dev/null +++ b/packages/oui-inline-adder/src/inline-adder.controller.js @@ -0,0 +1,57 @@ +import { addDefaultParameter } from "@ovh-ui/common/component-utils"; +import filter from "lodash/filter"; + +export default class { + constructor ($attrs, $element, $scope, $timeout, ouiInlineAdderConfiguration) { + "ngInject"; + + this.$attrs = $attrs; + this.$element = $element; + this.$scope = $scope; + this.$timeout = $timeout; + this.translations = ouiInlineAdderConfiguration.translations; + } + + $onInit () { + this.forms = [true]; + this.isDisabled = [false]; + + addDefaultParameter(this, "id", `ouiInlineAdderForm${this.$scope.$id}`); + addDefaultParameter(this, "name", `ouiInlineAdderForm${this.$scope.$id}`); + } + + $postLink () { + this.$timeout(() => + this.$element.addClass("oui-inline-adder") + ); + } + + onFormsChange () { + // Filter boolean values used for ngShow + const forms = filter(this.forms, (item) => angular.isObject(item)); + this.onChange({ forms }); + } + + onFormSubmit (form, index) { + if (form.$valid) { + this.forms[index] = form; + + // Create new instance of form + this.isDisabled[index] = true; + this.forms.push(true); + + // Callbacks + this.onAdd({ form }); + this.onFormsChange(); + } + } + + onFormRemove (form, index) { + // Hide removed form to avoid refreshing ngRepeat + this.forms[index] = false; + + // Callback + this.onRemove({ form }); + this.onFormsChange(); + } +} diff --git a/packages/oui-inline-adder/src/inline-adder.html b/packages/oui-inline-adder/src/inline-adder.html new file mode 100644 index 00000000..9502e156 --- /dev/null +++ b/packages/oui-inline-adder/src/inline-adder.html @@ -0,0 +1,24 @@ +
+
+
+ +
diff --git a/packages/oui-inline-adder/src/inline-adder.provider.js b/packages/oui-inline-adder/src/inline-adder.provider.js new file mode 100644 index 00000000..c2810a0f --- /dev/null +++ b/packages/oui-inline-adder/src/inline-adder.provider.js @@ -0,0 +1,25 @@ +import { merge } from "lodash"; + +export default class { + constructor () { + this.translations = { + ariaAddItem: "Add Item", + ariaRemoveItem: "Remove Item" + }; + } + + /** + * Set the translations + * @param {Object} translations a map of translations + */ + setTranslations (translations) { + this.translations = merge(this.translations, translations); + return this; + } + + $get () { + return { + translations: this.translations + }; + } +} diff --git a/packages/oui-inline-adder/src/row/inline-adder-row.component.js b/packages/oui-inline-adder/src/row/inline-adder-row.component.js new file mode 100644 index 00000000..4a62ecae --- /dev/null +++ b/packages/oui-inline-adder/src/row/inline-adder-row.component.js @@ -0,0 +1,16 @@ +export default { + controller: class { + constructor ($element, $timeout) { + "ngInject"; + + this.$element = $element; + this.$timeout = $timeout; + } + + $postLink () { + this.$timeout(() => + this.$element.addClass("oui-inline-adder__row") + ); + } + } +};