updating fork
Showing
6 changed files
with
200 additions
and
58 deletions
... | @@ -2,8 +2,8 @@ | ... | @@ -2,8 +2,8 @@ |
2 | "name": "rivets", | 2 | "name": "rivets", |
3 | "repo": "mikeric/rivets", | 3 | "repo": "mikeric/rivets", |
4 | "description": "Declarative data binding facility.", | 4 | "description": "Declarative data binding facility.", |
5 | "version": "0.4.5", | 5 | "version": "0.4.8", |
6 | "keywords": ["data binding", "template"], | 6 | "keywords": ["data binding", "templating"], |
7 | "scripts": ["lib/rivets.js"], | 7 | "scripts": ["lib/rivets.js"], |
8 | "main": "lib/rivets.js", | 8 | "main": "lib/rivets.js", |
9 | "license": "MIT" | 9 | "license": "MIT" | ... | ... |
1 | // rivets.js | 1 | // rivets.js |
2 | // version: 0.4.5 | 2 | // version: 0.4.8 |
3 | // author: Michael Richards | 3 | // author: Michael Richards |
4 | // license: MIT | 4 | // license: MIT |
5 | (function() { | 5 | (function() { |
6 | var Rivets, bindEvent, getInputValue, rivets, unbindEvent, | 6 | var Rivets, bindEvent, factory, getInputValue, unbindEvent, |
7 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, | 7 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, |
8 | __slice = [].slice, | 8 | __slice = [].slice, |
9 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; | 9 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; |
... | @@ -405,6 +405,15 @@ | ... | @@ -405,6 +405,15 @@ |
405 | 405 | ||
406 | getInputValue = function(el) { | 406 | getInputValue = function(el) { |
407 | var o, _i, _len, _results; | 407 | var o, _i, _len, _results; |
408 | if (window.jQuery != null) { | ||
409 | el = jQuery(el); | ||
410 | switch (el[0].type) { | ||
411 | case 'checkbox': | ||
412 | return el.is(':checked'); | ||
413 | default: | ||
414 | return el.val(); | ||
415 | } | ||
416 | } else { | ||
408 | switch (el.type) { | 417 | switch (el.type) { |
409 | case 'checkbox': | 418 | case 'checkbox': |
410 | return el.checked; | 419 | return el.checked; |
... | @@ -421,6 +430,7 @@ | ... | @@ -421,6 +430,7 @@ |
421 | default: | 430 | default: |
422 | return el.value; | 431 | return el.value; |
423 | } | 432 | } |
433 | } | ||
424 | }; | 434 | }; |
425 | 435 | ||
426 | Rivets.binders = { | 436 | Rivets.binders = { |
... | @@ -439,8 +449,9 @@ | ... | @@ -439,8 +449,9 @@ |
439 | return unbindEvent(el, 'change', this.currentListener); | 449 | return unbindEvent(el, 'change', this.currentListener); |
440 | }, | 450 | }, |
441 | routine: function(el, value) { | 451 | routine: function(el, value) { |
452 | var _ref; | ||
442 | if (el.type === 'radio') { | 453 | if (el.type === 'radio') { |
443 | return el.checked = el.value === value; | 454 | return el.checked = ((_ref = el.value) != null ? _ref.toString() : void 0) === (value != null ? value.toString() : void 0); |
444 | } else { | 455 | } else { |
445 | return el.checked = !!value; | 456 | return el.checked = !!value; |
446 | } | 457 | } |
... | @@ -455,8 +466,9 @@ | ... | @@ -455,8 +466,9 @@ |
455 | return unbindEvent(el, 'change', this.currentListener); | 466 | return unbindEvent(el, 'change', this.currentListener); |
456 | }, | 467 | }, |
457 | routine: function(el, value) { | 468 | routine: function(el, value) { |
469 | var _ref; | ||
458 | if (el.type === 'radio') { | 470 | if (el.type === 'radio') { |
459 | return el.checked = el.value !== value; | 471 | return el.checked = ((_ref = el.value) != null ? _ref.toString() : void 0) !== (value != null ? value.toString() : void 0); |
460 | } else { | 472 | } else { |
461 | return el.checked = !value; | 473 | return el.checked = !value; |
462 | } | 474 | } |
... | @@ -480,20 +492,27 @@ | ... | @@ -480,20 +492,27 @@ |
480 | return unbindEvent(el, 'change', this.currentListener); | 492 | return unbindEvent(el, 'change', this.currentListener); |
481 | }, | 493 | }, |
482 | routine: function(el, value) { | 494 | routine: function(el, value) { |
483 | var o, _i, _len, _ref, _results; | 495 | var o, _i, _len, _ref, _ref1, _ref2, _results; |
496 | if (window.jQuery != null) { | ||
497 | el = jQuery(el); | ||
498 | if ((value != null ? value.toString() : void 0) !== ((_ref = el.val()) != null ? _ref.toString() : void 0)) { | ||
499 | return el.val(value != null ? value : ''); | ||
500 | } | ||
501 | } else { | ||
484 | if (el.type === 'select-multiple') { | 502 | if (el.type === 'select-multiple') { |
485 | if (value != null) { | 503 | if (value != null) { |
486 | _results = []; | 504 | _results = []; |
487 | for (_i = 0, _len = el.length; _i < _len; _i++) { | 505 | for (_i = 0, _len = el.length; _i < _len; _i++) { |
488 | o = el[_i]; | 506 | o = el[_i]; |
489 | _results.push(o.selected = (_ref = o.value, __indexOf.call(value, _ref) >= 0)); | 507 | _results.push(o.selected = (_ref1 = o.value, __indexOf.call(value, _ref1) >= 0)); |
490 | } | 508 | } |
491 | return _results; | 509 | return _results; |
492 | } | 510 | } |
493 | } else { | 511 | } else if ((value != null ? value.toString() : void 0) !== ((_ref2 = el.value) != null ? _ref2.toString() : void 0)) { |
494 | return el.value = value != null ? value : ''; | 512 | return el.value = value != null ? value : ''; |
495 | } | 513 | } |
496 | } | 514 | } |
515 | } | ||
497 | }, | 516 | }, |
498 | text: function(el, value) { | 517 | text: function(el, value) { |
499 | if (el.innerText != null) { | 518 | if (el.innerText != null) { |
... | @@ -581,11 +600,11 @@ | ... | @@ -581,11 +600,11 @@ |
581 | 600 | ||
582 | Rivets.formatters = {}; | 601 | Rivets.formatters = {}; |
583 | 602 | ||
584 | rivets = { | 603 | factory = function(exports) { |
585 | binders: Rivets.binders, | 604 | exports.binders = Rivets.binders; |
586 | formatters: Rivets.formatters, | 605 | exports.formatters = Rivets.formatters; |
587 | config: Rivets.config, | 606 | exports.config = Rivets.config; |
588 | configure: function(options) { | 607 | exports.configure = function(options) { |
589 | var property, value; | 608 | var property, value; |
590 | if (options == null) { | 609 | if (options == null) { |
591 | options = {}; | 610 | options = {}; |
... | @@ -594,8 +613,8 @@ | ... | @@ -594,8 +613,8 @@ |
594 | value = options[property]; | 613 | value = options[property]; |
595 | Rivets.config[property] = value; | 614 | Rivets.config[property] = value; |
596 | } | 615 | } |
597 | }, | 616 | }; |
598 | bind: function(el, models) { | 617 | return exports.bind = function(el, models) { |
599 | var view; | 618 | var view; |
600 | if (models == null) { | 619 | if (models == null) { |
601 | models = {}; | 620 | models = {}; |
... | @@ -603,13 +622,18 @@ | ... | @@ -603,13 +622,18 @@ |
603 | view = new Rivets.View(el, models); | 622 | view = new Rivets.View(el, models); |
604 | view.bind(); | 623 | view.bind(); |
605 | return view; | 624 | return view; |
606 | } | 625 | }; |
607 | }; | 626 | }; |
608 | 627 | ||
609 | if (typeof module !== "undefined" && module !== null) { | 628 | if (typeof exports === 'object') { |
610 | module.exports = rivets; | 629 | factory(exports); |
630 | } else if (typeof define === 'function' && define.amd) { | ||
631 | define(['exports'], function(exports) { | ||
632 | factory(this.rivets = exports); | ||
633 | return exports; | ||
634 | }); | ||
611 | } else { | 635 | } else { |
612 | this.rivets = rivets; | 636 | factory(this.rivets = {}); |
613 | } | 637 | } |
614 | 638 | ||
615 | }).call(this); | 639 | }).call(this); | ... | ... |
This diff is collapsed.
Click to expand it.
1 | { | 1 | { |
2 | "name" : "rivets", | 2 | "name" : "rivets", |
3 | "description" : "Declarative data binding facility.", | 3 | "description" : "Declarative data binding facility.", |
4 | "version" : "0.4.5", | 4 | "version" : "0.4.8", |
5 | "author" : "Michael Richards", | 5 | "author" : "Michael Richards", |
6 | "url" : "http://rivetsjs.com", | 6 | "url" : "http://rivetsjs.com", |
7 | "main" : "./lib/rivets.js", | 7 | "main" : "./lib/rivets.js", | ... | ... |
1 | describe('Routines', function() { | 1 | describe('Routines', function() { |
2 | var el, input; | 2 | var el, input, trueRadioInput, falseRadioInput, checkboxInput; |
3 | |||
4 | var createInputElement = function(type, value) { | ||
5 | var elem = document.createElement('input'); | ||
6 | elem.setAttribute('type', type); | ||
7 | if (value !== undefined){ | ||
8 | elem.setAttribute('value', value); | ||
9 | } | ||
10 | document.body.appendChild(elem); | ||
11 | return elem; | ||
12 | }; | ||
3 | 13 | ||
4 | beforeEach(function() { | 14 | beforeEach(function() { |
5 | rivets.configure({ | 15 | rivets.configure({ |
... | @@ -12,8 +22,27 @@ describe('Routines', function() { | ... | @@ -12,8 +22,27 @@ describe('Routines', function() { |
12 | }); | 22 | }); |
13 | 23 | ||
14 | el = document.createElement('div'); | 24 | el = document.createElement('div'); |
15 | input = document.createElement('input'); | 25 | document.body.appendChild(el); |
16 | input.setAttribute('type', 'text'); | 26 | |
27 | input = createInputElement('text'); | ||
28 | |||
29 | // to test the radio input scenario when its value is "true" | ||
30 | trueRadioInput = createInputElement('radio', 'true'); | ||
31 | |||
32 | // to test the radio input scenario when its value is "false" | ||
33 | falseRadioInput = createInputElement('radio', 'false'); | ||
34 | |||
35 | // to test the checkbox input scenario | ||
36 | checkboxInput = createInputElement('checkbox'); | ||
37 | |||
38 | }); | ||
39 | |||
40 | afterEach(function(){ | ||
41 | el.parentNode.removeChild(el); | ||
42 | input.parentNode.removeChild(input); | ||
43 | trueRadioInput.parentNode.removeChild(trueRadioInput); | ||
44 | falseRadioInput.parentNode.removeChild(falseRadioInput); | ||
45 | checkboxInput.parentNode.removeChild(checkboxInput); | ||
17 | }); | 46 | }); |
18 | 47 | ||
19 | describe('text', function() { | 48 | describe('text', function() { |
... | @@ -126,33 +155,101 @@ describe('Routines', function() { | ... | @@ -126,33 +155,101 @@ describe('Routines', function() { |
126 | }); | 155 | }); |
127 | 156 | ||
128 | describe('checked', function() { | 157 | describe('checked', function() { |
129 | describe('with a truthy value', function() { | 158 | describe('with a checkbox input', function() { |
130 | it('checks the element', function() { | 159 | describe('and a truthy value', function() { |
131 | rivets.binders.checked.routine(el, true); | 160 | it('checks the checkbox input', function() { |
132 | expect(el.checked).toBe(true); | 161 | rivets.binders.checked.routine(checkboxInput, true); |
162 | expect(checkboxInput.checked).toBe(true); | ||
133 | }); | 163 | }); |
134 | }); | 164 | }); |
135 | 165 | ||
136 | describe('with a falsey value', function() { | 166 | describe('with a falsey value', function() { |
137 | it('unchecks the element', function() { | 167 | it('unchecks the checkbox input', function() { |
138 | rivets.binders.checked.routine(el, false); | 168 | rivets.binders.checked.routine(checkboxInput, false); |
139 | expect(el.checked).toBe(false); | 169 | expect(checkboxInput.checked).toBe(false); |
170 | }); | ||
171 | }); | ||
172 | }); | ||
173 | |||
174 | describe('with a radio input with value="true"', function() { | ||
175 | describe('and a truthy value', function() { | ||
176 | it('checks the radio input', function() { | ||
177 | rivets.binders.checked.routine(trueRadioInput, true); | ||
178 | expect(trueRadioInput.checked).toBe(true); | ||
179 | }); | ||
180 | }); | ||
181 | |||
182 | describe('with a falsey value', function() { | ||
183 | it('unchecks the radio input', function() { | ||
184 | rivets.binders.checked.routine(trueRadioInput, false); | ||
185 | expect(trueRadioInput.checked).toBe(false); | ||
186 | }); | ||
187 | }); | ||
188 | }); | ||
189 | |||
190 | describe('with a radio input with value="false"', function() { | ||
191 | describe('and a truthy value', function() { | ||
192 | it('checks the radio input', function() { | ||
193 | rivets.binders.checked.routine(falseRadioInput, true); | ||
194 | expect(falseRadioInput.checked).toBe(false); | ||
195 | }); | ||
196 | }); | ||
197 | |||
198 | describe('with a falsey value', function() { | ||
199 | it('unchecks the radio input', function() { | ||
200 | rivets.binders.checked.routine(falseRadioInput, false); | ||
201 | expect(falseRadioInput.checked).toBe(true); | ||
202 | }); | ||
140 | }); | 203 | }); |
141 | }); | 204 | }); |
142 | }); | 205 | }); |
143 | 206 | ||
144 | describe('unchecked', function() { | 207 | describe('unchecked', function() { |
145 | describe('with a truthy value', function() { | 208 | describe('and a truthy value', function() { |
146 | it('unchecks the element', function() { | 209 | describe('and a truthy value', function() { |
147 | rivets.binders.unchecked.routine(el, true); | 210 | it('checks the checkbox input', function() { |
148 | expect(el.checked).toBe(false); | 211 | rivets.binders.unchecked.routine(checkboxInput, true); |
212 | expect(checkboxInput.checked).toBe(false); | ||
213 | }); | ||
214 | }); | ||
215 | |||
216 | describe('with a falsey value', function() { | ||
217 | it('unchecks the checkbox input', function() { | ||
218 | rivets.binders.unchecked.routine(checkboxInput, false); | ||
219 | expect(checkboxInput.checked).toBe(true); | ||
220 | }); | ||
221 | }); | ||
222 | }); | ||
223 | |||
224 | describe('with a radio input with value="true"', function() { | ||
225 | describe('and a truthy value', function() { | ||
226 | it('checks the radio input', function() { | ||
227 | rivets.binders.unchecked.routine(trueRadioInput, true); | ||
228 | expect(trueRadioInput.checked).toBe(false); | ||
149 | }); | 229 | }); |
150 | }); | 230 | }); |
151 | 231 | ||
152 | describe('with a falsey value', function() { | 232 | describe('with a falsey value', function() { |
153 | it('checks the element', function() { | 233 | it('unchecks the radio input', function() { |
154 | rivets.binders.unchecked.routine(el, false); | 234 | rivets.binders.unchecked.routine(trueRadioInput, false); |
155 | expect(el.checked).toBe(true); | 235 | expect(trueRadioInput.checked).toBe(true); |
236 | }); | ||
237 | }); | ||
238 | }); | ||
239 | |||
240 | describe('with a radio input with value="false"', function() { | ||
241 | describe('and a truthy value', function() { | ||
242 | it('checks the radio input', function() { | ||
243 | rivets.binders.unchecked.routine(falseRadioInput, true); | ||
244 | expect(falseRadioInput.checked).toBe(true); | ||
245 | }); | ||
246 | }); | ||
247 | |||
248 | describe('with a falsey value', function() { | ||
249 | it('unchecks the radio input', function() { | ||
250 | rivets.binders.unchecked.routine(falseRadioInput, false); | ||
251 | expect(falseRadioInput.checked).toBe(false); | ||
252 | }); | ||
156 | }); | 253 | }); |
157 | }); | 254 | }); |
158 | }); | 255 | }); | ... | ... |
1 | # rivets.js | 1 | # rivets.js |
2 | # version : 0.4.5 | 2 | # version : 0.4.8 |
3 | # author : Michael Richards | 3 | # author : Michael Richards |
4 | # license : MIT | 4 | # license : MIT |
5 | 5 | ||
... | @@ -256,8 +256,15 @@ unbindEvent = (el, event, fn) -> | ... | @@ -256,8 +256,15 @@ unbindEvent = (el, event, fn) -> |
256 | event = 'on' + event | 256 | event = 'on' + event |
257 | el.detachEvent event, fn | 257 | el.detachEvent event, fn |
258 | 258 | ||
259 | # Returns the current input value for the specified element. | 259 | # Cross-browser input value getter. |
260 | getInputValue = (el) -> | 260 | getInputValue = (el) -> |
261 | if window.jQuery? | ||
262 | el = jQuery el | ||
263 | |||
264 | switch el[0].type | ||
265 | when 'checkbox' then el.is ':checked' | ||
266 | else el.val() | ||
267 | else | ||
261 | switch el.type | 268 | switch el.type |
262 | when 'checkbox' then el.checked | 269 | when 'checkbox' then el.checked |
263 | when 'select-multiple' then o.value for o in el when o.selected | 270 | when 'select-multiple' then o.value for o in el when o.selected |
... | @@ -279,7 +286,7 @@ Rivets.binders = | ... | @@ -279,7 +286,7 @@ Rivets.binders = |
279 | unbindEvent el, 'change', @currentListener | 286 | unbindEvent el, 'change', @currentListener |
280 | routine: (el, value) -> | 287 | routine: (el, value) -> |
281 | if el.type is 'radio' | 288 | if el.type is 'radio' |
282 | el.checked = el.value is value | 289 | el.checked = el.value?.toString() is value?.toString() |
283 | else | 290 | else |
284 | el.checked = !!value | 291 | el.checked = !!value |
285 | 292 | ||
... | @@ -291,7 +298,7 @@ Rivets.binders = | ... | @@ -291,7 +298,7 @@ Rivets.binders = |
291 | unbindEvent el, 'change', @currentListener | 298 | unbindEvent el, 'change', @currentListener |
292 | routine: (el, value) -> | 299 | routine: (el, value) -> |
293 | if el.type is 'radio' | 300 | if el.type is 'radio' |
294 | el.checked = el.value isnt value | 301 | el.checked = el.value?.toString() isnt value?.toString() |
295 | else | 302 | else |
296 | el.checked = !value | 303 | el.checked = !value |
297 | 304 | ||
... | @@ -311,9 +318,15 @@ Rivets.binders = | ... | @@ -311,9 +318,15 @@ Rivets.binders = |
311 | unbind: (el) -> | 318 | unbind: (el) -> |
312 | unbindEvent el, 'change', @currentListener | 319 | unbindEvent el, 'change', @currentListener |
313 | routine: (el, value) -> | 320 | routine: (el, value) -> |
321 | if window.jQuery? | ||
322 | el = jQuery el | ||
323 | |||
324 | if value?.toString() isnt el.val()?.toString() | ||
325 | el.val if value? then value else '' | ||
326 | else | ||
314 | if el.type is 'select-multiple' | 327 | if el.type is 'select-multiple' |
315 | o.selected = o.value in value for o in el if value? | 328 | o.selected = o.value in value for o in el if value? |
316 | else | 329 | else if value?.toString() isnt el.value?.toString() |
317 | el.value = if value? then value else '' | 330 | el.value = if value? then value else '' |
318 | 331 | ||
319 | text: (el, value) -> | 332 | text: (el, value) -> |
... | @@ -331,7 +344,7 @@ Rivets.binders = | ... | @@ -331,7 +344,7 @@ Rivets.binders = |
331 | "each-*": | 344 | "each-*": |
332 | block: true | 345 | block: true |
333 | bind: (el, collection) -> | 346 | bind: (el, collection) -> |
334 | el.removeAttribute ['data', rivets.config.prefix, @type].join('-').replace '--', '-' | 347 | el.removeAttribute ['data', Rivets.config.prefix, @type].join('-').replace '--', '-' |
335 | routine: (el, collection) -> | 348 | routine: (el, collection) -> |
336 | if @iterated? | 349 | if @iterated? |
337 | for view in @iterated | 350 | for view in @iterated |
... | @@ -350,12 +363,16 @@ Rivets.binders = | ... | @@ -350,12 +363,16 @@ Rivets.binders = |
350 | data[n] = m for n, m of @view.models | 363 | data[n] = m for n, m of @view.models |
351 | data[@args[0]] = item | 364 | data[@args[0]] = item |
352 | itemEl = el.cloneNode true | 365 | itemEl = el.cloneNode true |
353 | if @iterated.length > 0 | 366 | |
354 | previous = @iterated[@iterated.length - 1].els[0] | 367 | previous = if @iterated.length |
368 | @iterated[@iterated.length - 1].els[0] | ||
355 | else | 369 | else |
356 | previous = @marker | 370 | @marker |
371 | |||
357 | @marker.parentNode.insertBefore itemEl, previous.nextSibling ? null | 372 | @marker.parentNode.insertBefore itemEl, previous.nextSibling ? null |
358 | @iterated.push rivets.bind itemEl, data | 373 | view = new Rivets.View(itemEl, data) |
374 | view.bind() | ||
375 | @iterated.push view | ||
359 | 376 | ||
360 | "class-*": (el, value) -> | 377 | "class-*": (el, value) -> |
361 | elClass = " #{el.className} " | 378 | elClass = " #{el.className} " |
... | @@ -380,32 +397,36 @@ Rivets.config = | ... | @@ -380,32 +397,36 @@ Rivets.config = |
380 | Rivets.formatters = {} | 397 | Rivets.formatters = {} |
381 | 398 | ||
382 | # The rivets module. This is the public interface that gets exported. | 399 | # The rivets module. This is the public interface that gets exported. |
383 | rivets = | 400 | factory = (exports) -> |
384 | # Exposes the core binding routines that can be extended or stripped down. | 401 | # Exposes the core binding routines that can be extended or stripped down. |
385 | binders: Rivets.binders | 402 | exports.binders = Rivets.binders |
386 | 403 | ||
387 | # Exposes the formatters object to be extended. | 404 | # Exposes the formatters object to be extended. |
388 | formatters: Rivets.formatters | 405 | exports.formatters = Rivets.formatters |
389 | 406 | ||
390 | # Exposes the rivets configuration options. These can be set manually or from | 407 | # Exposes the rivets configuration options. These can be set manually or from |
391 | # rivets.configure with an object literal. | 408 | # rivets.configure with an object literal. |
392 | config: Rivets.config | 409 | exports.config = Rivets.config |
393 | 410 | ||
394 | # Sets configuration options by merging an object literal. | 411 | # Sets configuration options by merging an object literal. |
395 | configure: (options={}) -> | 412 | exports.configure = (options={}) -> |
396 | for property, value of options | 413 | for property, value of options |
397 | Rivets.config[property] = value | 414 | Rivets.config[property] = value |
398 | return | 415 | return |
399 | 416 | ||
400 | # Binds a set of model objects to a parent DOM element. Returns a Rivets.View | 417 | # Binds a set of model objects to a parent DOM element. Returns a Rivets.View |
401 | # instance. | 418 | # instance. |
402 | bind: (el, models = {}, options) -> | 419 | exports.bind = (el, models = {}, options = {}) -> |
403 | view = new Rivets.View(el, models, options) | 420 | view = new Rivets.View(el, models, options) |
404 | view.bind() | 421 | view.bind() |
405 | view | 422 | view |
406 | 423 | ||
407 | # Exports rivets for both CommonJS and the browser. | 424 | # Exports rivets for CommonJS, AMD and the browser. |
408 | if module? | 425 | if typeof exports == 'object' |
409 | module.exports = rivets | 426 | factory(exports) |
427 | else if typeof define == 'function' && define.amd | ||
428 | define ['exports'], (exports) -> | ||
429 | factory(@rivets = exports) | ||
430 | return exports | ||
410 | else | 431 | else |
411 | @rivets = rivets | 432 | factory(@rivets = {}) | ... | ... |
-
Please register or sign in to post a comment