Skip to content

Commit

Permalink
strand-repeater rewrite
Browse files Browse the repository at this point in the history
  • Loading branch information
Shuwen Qian committed Aug 1, 2016
1 parent 18cd573 commit d6d0520
Show file tree
Hide file tree
Showing 18 changed files with 651 additions and 404 deletions.
127 changes: 79 additions & 48 deletions docs/articles/repeater.md
Original file line number Diff line number Diff line change
@@ -1,67 +1,60 @@
# Using strand-repeater
# Working with strand-repeater

## Overview

`strand-repeater` allows the duplication of a set of fields contained within a template tag.

`strand-repeater-row` has three bindable properties (like `strand-grid-item`):
- `model`, a reference to javascript object with the data filling the row
- `scope`, a reference to the `strand-repeater` the row is contained in
- `index`, the index of the row's model within the repeater's `data`

So if you wanted to take a list of _n_ addresses, you could set up:
```html
<strand-repeater id="myRepeater">
<template preserve-content>
<strand-input name="first_name" placeholder="First Name"></strand-input>
<strand-input name="last_name" placeholder="Last Name"></strand-input>
<strand-input name="address" placeholder="Address"></strand-input>
<strand-input name="city" placeholder="City"></strand-input>
<strand-dropdown name="state" placeholder="State/Province">
...
</strand-dropdown>
<strand-input name="zip_code" placeholder="ZIP/Postal Code"></strand-input>
<strand-repeater-row model="{{model}}" scope="{{scope}}" index="{{index}}">
<span>{{index}}</span>
<strand-input name="first_name" placeholder="First Name" value="{{model.first_name}}"></strand-input>
<strand-input name="last_name" placeholder="Last Name" value="{{model.last_name}}"></strand-input>
<strand-input name="address" placeholder="Address" value="{{model.address}}"></strand-input>
<strand-input name="city" placeholder="City" value="{{model.city}}"></strand-input>
<strand-dropdown name="state" placeholder="State/Province" value="{{model.state}}">
...
</strand-dropdown>
<strand-input name="zip_code" placeholder="ZIP/Postal Code" value="{{model.zip_code}}"></strand-input>
</strand-repeater-row>
</template>
</strand-repeater>
```
Binding the values of the form input to the model means that the javascript object will update when the value of the form fields update.

## Validation
Like `strand-form`, `strand-repeater` takes a `config` object, which can take `validation` as a `string` or a custom validation method taking the arguments `value, row:Object, domref:HTMLElement`, and an `errorMessage`. If a custom validation method is used, `this.errorMessage` can be set dynamically.

```javascript
var myRepeater = document.getElementById('myRepeater'),
Validator = StrandLib.Validator;
myRepeater.config = {
'first_name': {
validation: 'alpha'
},
'last_name': {
validation: 'alpha'
},
'address': {
validation: function(value) {
var a = value.split(" "),
street_num = parseInt(a[0]),
street_name = a[1];
return street_num != NaN;
},
},
'city': {
validation: 'alpha'
},
'zip_code': {
validation: function(zip) {
var z = zip.replace('-','');
return parseInt(z) != NaN && (z.length == 5 || z.length == 9);
},
errorMessage: 'Not a valid ZIP'
}
}
Binding works just like in a custom `strand-grid-item`, so, for example, to disable a specific form field based on the value of a property in the model, you could write something like this:
```html
<strand-repeater>
<template preserve-content>
<strand-repeater-row model="{{model}}" scope="{{scope}}">
<strand-input name="name" placeholder="Name" value="{{model.name}}" disabled$="{{model.locked}}"></strand-input>
</strand-repeater-row>
</template>
</strand-repeater>
```
This will create an attribute binding to the `disabled` property of the input, so the user will not be able to modify the inputs of rows that are "locked".

## Getting data from `strand-repeater`
User data from repeated form fields are accessible through the `value` property on the `strand-repeater` element. `value` is a getter/setter interface for the `data` property—this ensures Polymer's data binding updates properly. Each object in the `value` array corresponds to a single repeater row, with key-value pairs corresponding to the name-value pairs of the form elements.
## Reading data from `strand-repeater`
`strand-repeater` has four public arrays that are exactly what they say on the tin:
- `data` contains all of the data in the repeater
- `added` contains only the models that have been added by the user
- `modified` contains only the pre-populated models that have been modified
- `deleted` contains the models that have been removed by the user (and are therefore no longer in `data`)
You will find these arrays on any element that implements the `Collection` behavior such as `strand-collection`. Each model also gets a `cId` property (distinct from the index) which will be useful to identify models [when validating](#validation).

## Getting data into `strand-repeater`
`strand-repeater` also has a `value` attribute that is just a getter/setter pair for `data`, allowing you to drop it into a larger `strand-form`.

Data can be preloaded into the repeater by setting the `value`. This is useful in views where the end user wishes to edit some pre-existing data.
## Pre-populating data
Data can be pre-populated into the repeater by setting `data`. This is useful in views where the user wishes to edit some pre-existing collection of data.
```javascript
var myRepeater = document.getElementById('myRepeater');
myRepeater.value = [
myRepeater.data = [
{
first_name: "Jerry",
last_name: "Seinfeld",
Expand All @@ -80,4 +73,42 @@ myRepeater.value = [
}
];
```
By default, `strand-repeater` initializes the `value` property with an array containing a single, empty `Object`. This results in a single, blank instance of the template being rendered when the form loads.
By default `data` will contain one array with an empty object, giving you a single row with all fields blank.

## Validation
You can assign a function to `strand-repeater`'s `validation` property, which the element will use to validate user input, set error state, and display error messages. When you call `validate()` on the repeater, that subsequently calls `validation` with four parameters, which are the `data`, `added`, `modified`, and `deleted` arrays (in that order) and should return an array of invalid models with error messages. There are two requirements for this to work properly:
- The `name` attribute of each form field you want to validate must match the name of the property its `value` is bound to. So in our example above, we have `<strand-input name="first_name" value="{{model.first_name}}">`.
- The models that `validation` returns must include the `cId` (which you can just pass through)

The easiest way to do this is with ES5 array `map` and `filter`:
```javascript
function validateAddr(address) {
if(!address) return 'Address required!';
var tmp = address.split(' ');
if(Number.isNaN(parseInt(tmp[0]))) return 'Street number required!';
}

function validateZip(zip) {
if(!zip) return 'ZIP required!';
var zip5 = zip.length === 5 && !Number.isNaN(parseInt(zip));
var zip9 = zip.length === 10 && zip.split('-').reduce(function(acc, current, index) {
return !Number.isNaN(parseInt(current)) && acc;
});
return (zip5 || zip9) ? null : 'ZIP is invalid!'
}

myRepeater.validation = function(data, added, modified, removed) {
return data.map(function(model) {
return {
cId: model.cId,
first_name: model.first_name ? null : 'First name required!',
last_name: model.last_name ? null : 'Last name required!',
address: validateAddr(model.address),
city: model.city ? null : 'City required!',
state: model.state ? null : 'State required!',
zip_code: validateZip(model.zip)
}
});
};
```
The repeater considers the field invalid if an error message is present. It will set the error state on the associated element if possible and add an error message below the form element, which will persist until the next call of `validate()`.
11 changes: 5 additions & 6 deletions src/shared/behaviors/collection.html
Original file line number Diff line number Diff line change
Expand Up @@ -164,19 +164,18 @@
_deepChanged:function(change) {
if (change && change.path) {
var path = change.path.split(".");
if (path.length > 1) {
var idx = parseInt(path[1].substr(1));
var model = this.data[idx];
if (path.length > 1 && path[1].charAt(0) === '#') {
var model = Polymer.Base.get('data.'+path[1], this);
this.fire('added-changed');
this.fire('removed-changed');
this.fire('modified-changed');
//dont move to modified list if its a 'new' model
if (model && this.getIndexOfCid(model.cId,'added') === -1
&& this.getIndexOfCid(model.cId, 'modified') === -1) {

this.push('modified', this.data[idx]);
this._cleanHelperArrays('added',this.data[idx]);
this._cleanHelperArrays('removed',this.data[idx]);
this.push('modified', model);
this._cleanHelperArrays('added', model);
this._cleanHelperArrays('removed', model);
}
} else if (change.path === 'data' && this.data) {
this._resetHelperArrays();
Expand Down
15 changes: 12 additions & 3 deletions src/shared/behaviors/validatable.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,24 @@
value: false
},
validation: {
type: String,
type: Object,
observer: "_validationChanged"
},

_customValidation: {
type: Boolean,
value: false
}
},

// items to validate against
testSet: null,

_validationChanged: function(newVal, oldVal) {
if (newVal) {
if (typeof newVal === 'string') {
this.testSet = newVal.replace(/\s/g, '').split("|");
} else if(typeof newVal === 'function') {
this.set('_customValidation', true);
}
},

Expand All @@ -46,7 +53,9 @@
},

validate: function(value) {
if(this.validation) {
if(this._customValidation) {
return this.validation(value);
} else if(this.validation) {
var result = this.testSet.map(function(item) {
return Validator.rules[item](value);
}, this).filter(function(item) {
Expand Down
9 changes: 5 additions & 4 deletions src/strand-dropdown/strand-dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
type:Boolean,
notify: true,
value: false
},
},
index: {
type:Number,
notify:true,
Expand All @@ -76,9 +76,9 @@
type: Boolean,
value: false
},
layout: {
layout: {
type: String,
reflectToAttribute: true
reflectToAttribute: true
},
data: {
type: Array,
Expand Down Expand Up @@ -154,7 +154,7 @@
},

_selectItemByValue: function(value) {
this.async(function(){
Polymer.RenderStatus.afterNextRender(this, function(){
var item = null;
var valueStr = value.toString();

Expand Down Expand Up @@ -264,6 +264,7 @@
var name = newSelected.name ? newSelected.name : newSelected.textContent.trim();

this.value = value;
this.error = false;

if (this.data) {
this.set('data.' + newIndex + '.selected', true);
Expand Down
2 changes: 1 addition & 1 deletion src/strand-form-message/strand-form-message.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
-->
<link rel="import" href="../../bower_components/polymer/polymer.html"/>
<link rel="import" href="../shared/behaviors/refable.html" />
<link rel="import" href="../shared/behaviors/resolvable.html" />
<link rel="import" href="../strand-inline-box/strand-inline-box.html" />

Expand All @@ -16,4 +17,3 @@
</dom-module>

<script src="strand-form-message.js"></script>

7 changes: 4 additions & 3 deletions src/strand-form-message/strand-form-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
*/
(function (scope) {

scope.FormMessage = Polymer({
is: 'strand-form-message',

behaviors: [
StrandTraits.Refable,
StrandTraits.Resolvable
],

Expand All @@ -25,7 +26,7 @@
value: false
}
},

});

})(window.Strand = window.Strand || {});
})(window.Strand = window.Strand || {});
8 changes: 7 additions & 1 deletion src/strand-form-message/strand-form-message.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,10 @@ strand-inline-box {
strand-inline-box[visible] {
display: block;
margin-top: 10px;
}
}

:host-context(strand-repeater) {
strand-inline-box[visible] {
margin-top: 4px;
}
}
46 changes: 46 additions & 0 deletions src/strand-repeater-row/strand-repeater-row.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!--
* @license
* Copyright (c) 2015 MediaMath Inc. All rights reserved.
* This code may only be used under the BSD style license found at http://mediamath.github.io/strand/LICENSE.txt
-->
<link rel="import" href="../../bower_components/polymer/polymer.html"/>
<link rel="import" href="../shared/fonts/fonts.html"/>
<link rel="import" href="../shared/behaviors/lightdomgettable.html"/>
<link rel="import" href="../shared/behaviors/refable.html"/>
<link rel="import" href="../shared/behaviors/resolvable.html"/>
<link rel="import" href="../shared/behaviors/templatefindable.html"/>
<link rel="import" href="../shared/behaviors/stylable.html"/>
<link rel="import" href="../shared/behaviors/validatable.html"/>
<link rel="import" href="../shared/js/validator.html"/>
<link rel="import" href="../shared/js/datautils.html"/>
<link rel="import" href="../strand-action/strand-action.html"/>
<link rel="import" href="../strand-box/strand-box.html"/>
<link rel="import" href="../strand-icon/strand-icon.html"/>
<link rel="import" href="../strand-form-message/strand-form-message.html"/>

<dom-module id="strand-repeater-row">
<link rel="import" type="css" href="strand-repeater-row.css" />
<template>
<div class$="{{_computeContainerClass(index, scope._last)}}">
<div class="row-wrapper">
<div class="content-wrapper">
<content id="content"></content>
</div>
<strand-box class="control" align="center">
<strand-action class="add-row" on-tap="_addRow" hidden$="{{!_shouldShowAddRow}}"><label>{{scope.addRowLabel}}</label></strand-action>
<template is="dom-if" if="{{scope._showRemove}}">
<strand-icon class="remove-row" type="delete" on-tap="_removeRow" height="12" width="12"></strand-icon>
</template>
</strand-box>
</div>
<div class="error-container row-wrapper">
<template is="dom-repeat" id="errorRepeat" items="{{errors}}">
<strand-form-message class="error-message" type="error" visible$="{{item.message}}" message="{{item.message}}" style$="{{_computeErrorMessageStyle(item)}}"></strand-form-message>
</template>
</div>
</div>
</template>
</dom-module>

<script src="strand-repeater-row.js"></script>
Loading

0 comments on commit d6d0520

Please sign in to comment.