From 0eae9d47d8e509e17ee83203c9c4c615573fd381 Mon Sep 17 00:00:00 2001 From: Todd Jordan Date: Wed, 10 Feb 2016 13:24:44 -0500 Subject: [PATCH] Add ability to use closure actions to react to form value changes start over after wrong turn Add note on jquery resolution suggested resolution for deprecation causes 2 tests to fail fixed test failure Add on change action support Add documentation for new change action Fix phantom test simplify gatering of fields add action on keyup and click --- README.md | 2 + addon/components/dynamic-form.js | 45 +++++++-- package.json | 1 + .../app/controllers/demos/change-action.js | 13 +++ tests/dummy/app/router.js | 1 + tests/dummy/app/routes/demos/change-action.js | 46 +++++++++ tests/dummy/app/templates/demos.hbs | 3 +- .../dummy/app/templates/demos/basic-usage.hbs | 2 +- .../app/templates/demos/change-action.hbs | 97 +++++++++++++++++++ .../{actions-test.js => buttons-test.js} | 0 .../components/change-action-test.js | 64 ++++++++++++ 11 files changed, 265 insertions(+), 9 deletions(-) mode change 100644 => 100755 package.json create mode 100644 tests/dummy/app/controllers/demos/change-action.js create mode 100644 tests/dummy/app/routes/demos/change-action.js create mode 100644 tests/dummy/app/templates/demos/change-action.hbs rename tests/integration/components/{actions-test.js => buttons-test.js} (100%) create mode 100644 tests/integration/components/change-action-test.js diff --git a/README.md b/README.md index 3d32117..811f88a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ An Ember addon for creating dynamic forms, powered by [alpacajs](http://alpacajs `ember install ember-cli-dynamic-forms` +For now, you'll get a jquery resolution prompt when running install. You'll want to pick the 1.11.3 option. + ## Upgrading It's advisable to run `ember g ember-cli-dynamic-forms` between upgrades as dependencies may have been added, removed, or upgraded between releases. Please try this, along with clearing node_modules and bower_components before reporting issues after upgrading. diff --git a/addon/components/dynamic-form.js b/addon/components/dynamic-form.js index a848266..88565a4 100644 --- a/addon/components/dynamic-form.js +++ b/addon/components/dynamic-form.js @@ -1,4 +1,5 @@ import Ember from 'ember'; +import getOwner from 'ember-getowner-polyfill'; const TYPE_MAP = { validator: { @@ -21,12 +22,46 @@ const DynamicForm = Ember.Component.extend({ didReceiveAttrs() { let schemaObj = this._initSchema(this.get('schema')); - const schemaWithActions = this._addActions(schemaObj); + const schemaWithPostRender = this._buildPostRender(schemaObj); + const schemaWithActions = this._addActions(schemaWithPostRender); const filteredSchema = this._processFilters(schemaWithActions); const mappedSchema = this._replaceKeywordsWithFunctions(filteredSchema); this.set('renderSchema', mappedSchema); }, + _buildPostRender(schemaObj) { + let postRenderFns = []; + if (this.get('changeAction')) { + let fields = Object.keys(schemaObj.schema.properties); + const changeAction = this.get('changeAction'); + let changeFn = function (control) { + fields.forEach((field) => { + control.childrenByPropertyId[field].on('keyup', function (e) { + changeAction(e, field); + }); + control.childrenByPropertyId[field].on('click', function (e) { + changeAction(e, field); + }); + }); + }; + postRenderFns.push(changeFn); + } + if (this.get('postRender')) { + postRenderFns.push(this.get('postRender')); + } + + if (postRenderFns.length > 0) { + if (schemaObj.postRender) { + postRenderFns.push(schemaObj.postRender); + } + schemaObj.postRender = function () { + const args = arguments; + postRenderFns.forEach((fn) => fn(args[0])); + }; + } + return schemaObj; + }, + _addActions(schemaObj) { return _.reduce(this.get('formActions'), (result, value, key) => { if ((((((result || {}).options || {}).form || {}).buttons || {})[key])) { @@ -44,7 +79,7 @@ const DynamicForm = Ember.Component.extend({ const newSchema = _.reduce(optionFields, (result, val, key) => { if(val['filter-rules']) { val['filter-rules'].forEach((element) => { - const filterRule = this.container.lookup(`${element}:dynamic-forms.filter-rules`); + const filterRule = getOwner(this).lookup(`${element}:dynamic-forms.filter-rules`); filterRule.filter(key, result); }); } @@ -60,10 +95,6 @@ const DynamicForm = Ember.Component.extend({ } else { schemaObj = _.clone(schema, true); } - if (this.get('postRender')) { - const postRenderFn = this.get('postRender'); - schemaObj.postRender = postRenderFn; - } if (this.get('data')) { schemaObj.data = this.get('data'); } @@ -71,7 +102,7 @@ const DynamicForm = Ember.Component.extend({ }, _replaceKeywordsWithFunctions(schemaObj) { - const container = this.container; + const container = getOwner(this); const replaceWithFunction = function (object, value, key) { if (TYPE_MAP.hasOwnProperty(key) && typeof value === 'string') { const type = TYPE_MAP[key]; diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 86b79ae..e94b3d0 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "ember-disable-prototype-extensions": "^1.0.0", "ember-disable-proxy-controllers": "^1.0.1", "ember-export-application-global": "^1.0.4", + "ember-getowner-polyfill": "1.0.0", "ember-resolver": "^2.0.3", "ember-try": "~0.0.8" }, diff --git a/tests/dummy/app/controllers/demos/change-action.js b/tests/dummy/app/controllers/demos/change-action.js new file mode 100644 index 0000000..d34df25 --- /dev/null +++ b/tests/dummy/app/controllers/demos/change-action.js @@ -0,0 +1,13 @@ +import Ember from 'ember'; + +export default Ember.Controller.extend({ + actions: { + updateOnChange(event) { + Ember.Logger.debug('changed', event); + const propertyName = event.target.name; + const value = event.target.value; + Ember.Logger.debug('sending updateModel actions with ', propertyName, value); + this.send('updateModel', propertyName, value); + } + } +}); diff --git a/tests/dummy/app/router.js b/tests/dummy/app/router.js index 2cd275f..d00a621 100644 --- a/tests/dummy/app/router.js +++ b/tests/dummy/app/router.js @@ -13,6 +13,7 @@ Router.map(function() { this.route('basic-usage'); this.route('data'); this.route('actions'); + this.route('change-action'); }); this.route('demo', function() {}); diff --git a/tests/dummy/app/routes/demos/change-action.js b/tests/dummy/app/routes/demos/change-action.js new file mode 100644 index 0000000..6c5ccd2 --- /dev/null +++ b/tests/dummy/app/routes/demos/change-action.js @@ -0,0 +1,46 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({ + data: Ember.Object.create({ + name: 'Todd Jordan', + feedback: 'Ember + Alpaca = Awesome', + ranking: 'excellent' + }), + + actions: { + updateModel(propertyName, value) { + Ember.Logger.debug('changed', propertyName, value); + this.set(`data.${propertyName}`, value); + } + }, + + model() { + const schema = { + "schema": { + "title":"User Feedback", + "description":"What do you think about Alpaca?", + "type":"object", + "properties": { + "name": { + "type":"string", + "title":"Name" + }, + "feedback": { + "type":"string", + "title":"Feedback" + }, + "ranking": { + "type":"string", + "title":"Ranking", + "enum":['excellent','ok','so so'] + } + } + } + }; + + return { + schema, + data: this.get('data') + }; + } +}); diff --git a/tests/dummy/app/templates/demos.hbs b/tests/dummy/app/templates/demos.hbs index 3c77b7b..e68a011 100644 --- a/tests/dummy/app/templates/demos.hbs +++ b/tests/dummy/app/templates/demos.hbs @@ -10,7 +10,8 @@ {{#link-to "demos.formatting" class="list-group-item"}}Formatting{{/link-to}} {{#link-to "demos.data" class="list-group-item"}}Data{{/link-to}} {{#link-to "demos.filter-rules" class="list-group-item"}}Filter Rules{{/link-to}} - {{#link-to "demos.actions" class="list-group-item"}}Form Actions{{/link-to}} + {{#link-to "demos.actions" class="list-group-item"}}Button Actions{{/link-to}} + {{#link-to "demos.change-action" class="list-group-item"}}Change Action{{/link-to}} diff --git a/tests/dummy/app/templates/demos/basic-usage.hbs b/tests/dummy/app/templates/demos/basic-usage.hbs index 24fb099..451615c 100644 --- a/tests/dummy/app/templates/demos/basic-usage.hbs +++ b/tests/dummy/app/templates/demos/basic-usage.hbs @@ -70,6 +70,6 @@ export default Ember.Route.extend({

Once the object is made available, you can use it within your template to render the form.

{{#highlight-js}} -
/app/templates/demos/basic-usage.hbs\{{dynamic-form schema=model.basicObject}}
+
/app/templates/demos/basic-usage.hbs\{{dynamic-form schema=model}}
{{/highlight-js}} diff --git a/tests/dummy/app/templates/demos/change-action.hbs b/tests/dummy/app/templates/demos/change-action.hbs new file mode 100644 index 0000000..2880466 --- /dev/null +++ b/tests/dummy/app/templates/demos/change-action.hbs @@ -0,0 +1,97 @@ +

Change Actions

+ +

This form has a closure action that will update the text below as the form is updated

+ +{{dynamic-form schema=model.schema data=model.data changeAction=(action "updateOnChange") }} + +

Model Values

+ + + +
+ +

The dynamic-form component supports the data down actions up (DDAU) pattern by taking an action that will be executed whenever a rendered form field changes.

+ +

In the example our controller defines a closure action that will receive events from our dynamic form component, pull out the field name and value, and then send an action to the server.

+ +{{#highlight-js}} +
/app/controller/demos/change-action.js
+import Ember from 'ember';
+
+export default Ember.Controller.extend({
+  actions: {
+    updateOnChange(event) {
+      const propertyName = event.target.name;
+      const value = event.target.value;
+      this.send('updateModel', propertyName, value);
+    }
+  }
+});
+  
+{{/highlight-js}} + +

The route sets up a dynamic form schema as well as data to prepopulate it with. It also uses the update action to update the data model.

+ +{{#highlight-js}} +
/app/routes/demos/change-action.js
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+  data:  Ember.Object.create({
+  name: 'Todd Jordan',
+  feedback: 'Ember + Alpaca = Awesome',
+    ranking: 'excellent'
+  }),
+
+  actions: {
+    updateModel(propertyName, value) {
+      Ember.Logger.debug('changed', propertyName, value);
+      this.set(`data.${propertyName}`, value);
+    }
+  },
+
+  model() {
+    const schema = {
+      "schema": {
+        "title":"User Feedback",
+        "description":"What do you think about Alpaca?",
+        "type":"object",
+        "properties": {
+          "name": {
+            "type":"string",
+            "title":"Name"
+          },
+          "feedback": {
+            "type":"string",
+            "title":"Feedback"
+          },
+          "ranking": {
+            "type":"string",
+            "title":"Ranking",
+            "enum":['excellent','ok','so so']
+          }
+        }
+      }
+    };
+
+    return {
+      schema,
+      data: this.get('data')
+    };
+  }
+});
+
+{{/highlight-js}} + +

Finally, the template defines a dynamic-form with a closure action that will provide the controller action.

+ +{{#highlight-js}} +
/app/templates/demos/change-action.hbs\{{dynamic-form schema=model.schema
+    data=model.data
+    changeAction=(action "updateOnChange")}}
+
+{{/highlight-js}} diff --git a/tests/integration/components/actions-test.js b/tests/integration/components/buttons-test.js similarity index 100% rename from tests/integration/components/actions-test.js rename to tests/integration/components/buttons-test.js diff --git a/tests/integration/components/change-action-test.js b/tests/integration/components/change-action-test.js new file mode 100644 index 0000000..1023a15 --- /dev/null +++ b/tests/integration/components/change-action-test.js @@ -0,0 +1,64 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +const schemaObject = { + "schema": { + "title": "What do you think of Alpaca?", + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "ranking": { + "type": "string", + "title": "Ranking", + "enum": ['excellent', 'not too shabby', 'alpaca built my hotrod'] + } + } + } + +}; + +moduleForComponent('/dynamic-form' , 'Integration | Component | dynamic-form:changeAction', { + integration: true +}); + +test('should fire action on changes in form', function (assert) { + const done = assert.async(); + this.on('changeAction', function (event) { + assert.equal(event.target.value, 'todd'); + done(); + }); + + const schema = _.clone(schemaObject, true); + let postRender = () => { + this.$('.alpaca-field-text input').val('todd').keyup(); + }; + this.set('postRender', postRender); + this.set('schema', schema); + + this.render(hbs` + {{dynamic-form postRender=postRender schema=schema changeAction=(action 'changeAction')}} + `); + +}); + +test('should fire action and perform postRender when defined on schema', function (assert) { + const done = assert.async(); + this.on('changeAction', function (event) { + assert.equal(event.target.value, 'todd'); + done(); + }); + + const schema = _.clone(schemaObject, true); + schema.postRender = () => { + this.$('.alpaca-field-text input').val('todd').keyup(); + }; + this.set('schema', schema); + + this.render(hbs` + {{dynamic-form schema=schema changeAction=(action 'changeAction')}} + `); + +});