JSFiddle - React, Tailwind, and code Playground
HTML
<script src="https://rawgithub.com/Knockout-Contrib/Knockout-Validation/master/Dist/knockout.validation.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.5.2/underscore-min.js"></script>
<script type="text/html" id="validationErrorMessage">
<p data-bind="visible: field.isModified() && !field.isValid(),
text: field.error" class="validationMessage"></p>
</script>
<h1>Abstract Submission Form</h1>
<form id='AbstractForm' data-bind="submit: onSubmit">
<section>
<h3>Registration-Id:</h3>
<!--ko text: id --><!--/ko-->
<input type="hidden" name="id" data-bind="value: id">
</section>
<section>
<h3>Presentation Title:</h3>
<textarea data-bind="value: title" name="title"></textarea>
</section>
<section>
<h3>Author List:</h3>
<table id="authors">
<tr><th>Presenter</th><th>Initial</th><th>Name</th><th>Affiliations</th></tr>
<!--ko foreach: authors-->
<tr data-bind='validationOptions: { insertMessages: false }'>
<td> <!--ko text: $index() + 1 + '.' --><!--/ko-->
<input type="radio" name="presenter" data-bind="checked: $parent.presenter, value: $index()"></td>
<td><input data-bind="value: initial, attr: {name:'author['+$index()+'].initial', placeholder: $parent.authorPlaceHolder($index())[0]}"></td>
<td><input data-bind="value: name, attr:{name:'author['+$index()+'].name', placeholder: $parent.authorPlaceHolder($index())[1]}"></td>
<td><input data-bind="value: affiliations, attr:{name:'author['+$index()+'].affiliations', placeholder: $parent.authorPlaceHolder($index())[2]}"></td>
<td>
<a href="" data-bind="click: $parent.authorRemove,...
CSS
/* リセットしてしまった見出しのサイズ調整 */
h1 {
font-size: 160%;
}
h2 {
font-size: 130%;
}
h3 {
font-size: 100%;
}
/* ここからが AbstractForm 関連 */
form#AbstractForm h3 {
margin: 20px 0 10px 0;
}
form#AbstractForm textarea[name='title'] {
width: 450px;
height: 3em;
}
input.validationError,
select.validationError,
textarea.validationError {
background-color: #fcc;
}
form#AbstractForm {
line-height: 140%;
}
form#AbstractForm input,
form#AbstractForm select,
form#AbstractForm textarea {
font-size: 100%;
}
form#AbstractForm section > :not(h3) {
margin-left: 20px;
}
form#AbstractForm table#authors tr th,
form#AbstractForm table#authors tr td,
form#AbstractForm table#affiliations tr th,
form#AbstractForm table#affiliations tr td {
padding: 0 5px 0 5px;
font-weight: normal;
}
form#AbstractForm table#authors tr th,
form#AbstractForm table#affiliations tr th {
vertical-align: top;
}
form#AbstractForm table#authors tr td:nth-child(2) input {
width: 60px;
}
form#AbstractForm table#authors tr td:nth-child(3) input {
width: 150px;
}
form#AbstractForm table#authors tr td:nth-child(4) input {
width: 80px;
}
form#AbstractForm table#affiliations tr td:nth-child(2) input {
width: 300px;
}
form#AbstractForm table#affiliations tr td:nth-child(3) input {
width: 100px;
}
form#AbstractForm section#preview div.title {
font-weight: bold;
}
form#AbstractForm section#preview div.authors,
form#AbstractForm section#preview div.affiliations {
font-style: italic;
}
form#AbstractForm section#preview > div.authors > span.presenter {
text-decoration: underline;
}
form#AbstractForm section#preview > div.authors > span.author > span.affiliations {
vertical-align: super;
font-size: small;
}
form#AbstractForm section#preview > div.authors > span.presenter > span.affiliations {
text-decoration: none !important;
}
form#AbstractForm section#preview div.affiliations span.affiliation span.number {
...
JavaScript
var Author = function(form, initial, name, affiliations){
var self = this;
// self.form = form; // これをすると依存関係の検出時に無限ループに落ち込む
// self.form = function(){ return form; } // これならOK
self.initial = ko.observable(initial).extend({
required: true,
pattern: {
message: 'First and middle names should be typed as initial(s) '+
'that is expected to have a period character "." at the end.',
params: '\\.$'
}
});
self.name = ko.observable(name).extend({ required: true });
self.affiliations = ko.observable(affiliations).extend({
required: true,
pattern: {
message: 'Affiliation must be specified by comma separated numbers.',
params: '^ *[1-9][0-9]*( *, *[1-9][0-9]*)* *$'
},
validation: [ {
validator: function(val) {
var ok = true;
_.each(val.split(/ *, */), function(a) {
if(Number(a) > form.affiliations().length ||
(Number(a) == 0) )
ok = false;
});
return ok;
},
message: 'Specified affiliation number does not exist.'
}, {
validator: function(val) {
var as = _.sortBy(
_.map( val.split(/ *, */),
function(a) { return Number(a); }),
function(a) {return a});
for(var i=0; i<as.length-1; i++)
if(as[i]==as[i+1])
...