ManuelDeLeon / viewmodel

MVVM for Meteor
https://viewmodel.org
MIT License
205 stars 23 forks source link

Props with validation - feature request #236

Closed comerc closed 8 years ago

comerc commented 8 years ago

Example:

TemplateController('message_count', {
  props: new SimpleSchema({
    messageCount: {
      type: Number, // allows only integers!
      defaultValue: 0
    }
  })
});
ManuelDeLeon commented 8 years ago

I think it's about time for this. Here's what I'm thinking to do regarding schemas and validations:

Template.Adult.viewmodel({
  name: '', // property is infered to be ViewModel.property.text
  age: ViewModel.property.number.min(18).default(18), // default value provided
  email: ViewModel.property.email.required, // email defaults to ''
  password: ViewModel.property.text.min(10).max(50).required, // text defaults to ''
  cards: 52, // property is infered to be ViewModel.property.number

  // Should the view model accept properties that aren't part of the initial specification?
  // Default: false
  acceptNewProperties: true
})
// Set acceptNewProperties for all view models:
ViewModel.acceptNewProperties = true;

So in the case of the phone book example, the login view model would be simplified like this:

Template.login.viewmodel({
  mixin: 'email',
  isNew: true,
  signHover: false,
  signText: function() {
    return this.isNew() ? 'Sign Up' : 'Sign In';
  },

  name: ViewModel.property
    .text
    .message('Name is required')
    .validate(() => { !this.isNew() || !!this.name() }),

  email: ViewModel.property.email.message('Valid email is required'),

  password: ViewModel.property
    .text
    .message('Password must be at least 8 characters long')
    .required
    .min(8),

  enter: function() {
    if (! this.valid()) return;

    if (this.isNew()){
      Accounts.createUser({
        email: this.email(),
        password: this.password(),
        profile: {
          name: this.name()
        }
      }, function(err){
        if (err) {
          toastr.error("Could not create your user:<br>" + err.reason);
        }
      })
    } else {
      Meteor.loginWithPassword(this.email(), this.password(), function(err){
        if (err) {
          toastr.error("Could not log you in:<br>" + err.reason);
        }
      });
    }
  }
});

I'm also thinking of adding a few extra stuff like:

  prop: ViewModel.property
    .beforeUpdate( (newValue) => { } )
    .afterUpdate( (oldValue) => { } )
    .throttle(500)
comerc commented 8 years ago

Yes! I want to suggest more extra stuff from SimpleSchema Rules: decimal, exclusiveMin/exclusiveMax, minCount/maxCount, allowedValues, regEx, trim, autoValue.

ManuelDeLeon commented 8 years ago

What's the difference between decimal and number? What on earth is exclusiveMin/Max? (I read the docs and I still have no clue)

comerc commented 8 years ago

decimal(false) - is integer, but decimal(true) - is float exclusiveMin/Max - allow value wo range: a < exclusiveMin || a > exclusiveMax

ManuelDeLeon commented 8 years ago

No decimal. Just number and integer. I still have no clue what exclusiveMin/Max is. Can you give a real example?

comerc commented 8 years ago

Example: parking is prohibited from 8:00 to 23:00

ManuelDeLeon commented 8 years ago

That's an odd way of saying between and notBetween. I'll include the functionality tho (with a different name of course). On Apr 21, 2016 3:49 PM, "comerc" notifications@github.com wrote:

Example: parking is prohibited from 8:00 to 23:00

— You are receiving this because you commented. Reply to this email directly or view it on GitHub https://github.com/ManuelDeLeon/viewmodel/issues/236#issuecomment-213126901

comerc commented 8 years ago

We need validation rules in model-level. Yes! SimpleSchema provide them. But separate validation rules in ViewModel level - it is overhead. No? May be best way is mapping ViewModel props with SimpleSchema fields.

ManuelDeLeon commented 8 years ago

No way I'll add a schema package as a dependency of ViewModel. Even if I thought it was a good idea to include the dependency, SimpleSchema deals with DB models while this library deals with VIEW models. They're different and you shouldn't conflate the two.

daveeel commented 8 years ago

Astronomy provides a complete solution for schema, validation and other data related operations. Using them in VM template is easy and clean. I am not too sure about the value of adding schema/validation to VM as it should focus on View. I'd suggest to make it an optional package if it's implemented.

comerc commented 8 years ago

OK. We need plugin-mechanism for validation from external model-package. And we need mapping of ViewModel.props with Model.fields - one for two cases: for use validation and save data.

Instead of:

    var contact = {
      name: this.name(),
      number: this.number(),
      email: this.email(),
      categoryId: this.categoryId()
    };
comerc commented 8 years ago
Template.Adult.viewmodel({
  age: this.mapTo('adult.age'), 
  // where 'adult.age' is model.field name
  validateCallback: function(fieldName, value) { return true; }, 
  // fieldName is 'adult.age', value is this.age()
  modelDataCallback: function(fieldName, value) { return value; }
  // it allow transform value if necessary
});

And then this.getModelData() return data for save to Model from mapped props.

ManuelDeLeon commented 8 years ago

@daveeel

I'm not talking about data/entities schemas. I want to add "view" validations which may or may not relate to your application's entities. For example, I've always found the following piece of code a bit awkward:

  message: '',
  messageValid() {
    return !!this.message() && this.message().length > 8;
  },
  logMessageIfValid() {
    if ( this.messageValid() ) console.log( this.message() );
  }

Remember that there may or may not be a "messages" collection in your data store. I think it's much cleaner to do:

  message: ViewModel.property.text.min(8),
  logMessageIfValid() {
    if ( this.message.valid() ) console.log( this.message() );
  }
daveeel commented 8 years ago

Now I see what you mean. Agreed !

comerc commented 8 years ago

Implementation of validation with SimpleSchema:

Nodes = new (Mongo.Collection)('nodes')

NodeSchema = new SimpleSchema
  fieldHead:
    type: String
    max: 3
  fieldBody:
    type: String
    min: 3

Nodes.attachSchema(NodeSchema)

Template.hello.viewmodelEvents =
  'submit form': ->
    data = @getModelData()
    result = validationContext.validate(data)
    console.log 'submit', data, result
    return false

validationContext = Nodes.simpleSchema().newContext()
map =
  'propHead': 'fieldHead'
  'propBody': 'fieldBody'

Template.hello.viewmodel
  propHead: ''
  propBody: ''
  validateCallback: (fieldName, value) ->
    obj = {}
    obj[fieldName] = value
    return validationContext.validateOne(obj, fieldName)
  validate: (propName) ->
    fieldName = map[propName]
    return @validateCallback(fieldName, @[propName]())
  modelDataCallback: (fieldName, value) ->
    return value
  getModelData: ->
    result = {}
    for propName, fieldName of map
      result[fieldName] = @modelDataCallback(fieldName, @[propName]())
    return result
  events: Template.hello.viewmodelEvents

ViewModel.helperName = 'b'
template(name="hello")
  form
    input($b="value: propHead, class: { error: !validate('propHead') }")
    input($b="value: propBody, class: { error: !validate('propBody') }")
    button(type="submit") Submit

What do you think?

comerc commented 8 years ago

OK. It is not true. We need validate prop when set value and store validation state.

ManuelDeLeon commented 8 years ago

@comerc Once VM validations comes out that whole thing will be reduced to:

Template.hello.viewmodel
  propHead: ViewModel.property.text.max(3)
  propBody: ViewModel.property.text.min(3)
  submit: ->
    data = this.data()
    result = this.valid()
    console.log 'submit', data, result
template(name="hello")
  form
    input($b="value: propHead, class: { error: !propHead.valid }")
    input($b="value: propBody, class: { error: !propBody.valid }")
    button($b="click: submit") Submit

Once again, ViewModel validations and SimpleSchema validations have different concerns. One deals with views and the other with dbs.

comerc commented 8 years ago

Validation must not depend of ViewModel. I found, where I need callback for realization external validation. It is named setValueCallback. Please waiting. I write demo.

viewmodel.coffee

  setValue = (value, container, bindValue, viewmodel) ->
    # console.log value, container, bindValue, viewmodel
    if dotRegex.test(bindValue)
      i = bindValue.search(dotRegex)
      i += 1 if bindValue.charAt(i) isnt '.'
      newContainer = getValue container, bindValue.substring(0, i), viewmodel
      newBindValue = bindValue.substring(i + 1)
      setValue value, newContainer, newBindValue, viewmodel
    else
      # !!!
      container.setValueCallback(bindValue, value) if container.setValueCallback?
      if _.isFunction(container[bindValue]) then container[bindValue](value) else container[bindValue] = value
    return
ManuelDeLeon commented 8 years ago

I have no clue of what you mean. On Apr 24, 2016 3:33 PM, "comerc" notifications@github.com wrote:

Validation must not depend of ViewModel. I found, where I need callback for realization external validation. It is named setValueCallback. Please waiting. I write demo.

viewmodel.coffee

setValue = (value, container, bindValue, viewmodel) ->

console.log value, container, bindValue, viewmodel

if dotRegex.test(bindValue)
  i = bindValue.search(dotRegex)
  i += 1 if bindValue.charAt(i) isnt '.'
  newContainer = getValue container, bindValue.substring(0, i), viewmodel
  newBindValue = bindValue.substring(i + 1)
  setValue value, newContainer, newBindValue, viewmodel
else
  container.setValueCallback(bindValue, value) if container.setValueCallback?
  if _.isFunction(container[bindValue]) then container[bindValue](value) else container[bindValue] = value
return

— You are receiving this because you commented. Reply to this email directly or view it on GitHub https://github.com/ManuelDeLeon/viewmodel/issues/236#issuecomment-214041429

fvpDev commented 8 years ago

Lol

comerc commented 8 years ago

OK, I am released TemplateTwoWayBinding with required features. :)

comerc commented 8 years ago

Demo here!

hluz commented 8 years ago

Great job with 4.1.0! Validations working great so far! Many thanks!

ManuelDeLeon commented 8 years ago

Lol, I didn't announce it because I screwed up the documentation site. I'm glad you like it. On May 5, 2016 10:29 PM, "hluz" notifications@github.com wrote:

Great job with 4.1.0! Validations working great so far! Many thanks!

— You are receiving this because you commented. Reply to this email directly or view it on GitHub https://github.com/ManuelDeLeon/viewmodel/issues/236#issuecomment-217345721

hluz commented 8 years ago

Noticed that... Thanks god I "grabbed" the docs while it was up and had it running for a while in one browser tab... Unfortunately though, I pressed the browser back key while on that tab and when trying to go forward again got the server error... :-(

ManuelDeLeon commented 8 years ago

It's working now (knock wood). I wanted to include the React part of the docs but in my scrambling left it out.

On Thu, May 5, 2016 at 11:23 PM, hluz notifications@github.com wrote:

Noticed that... Thanks god I "grabbed" the docs while it was up and had it running for a while in one browser tab... Unfortunately though, I pressed the browser back key while on that tab and when trying to go forward again got the server error... :-(

— You are receiving this because you commented. Reply to this email directly or view it on GitHub https://github.com/ManuelDeLeon/viewmodel/issues/236#issuecomment-217350721

hluz commented 8 years ago

.validate(function(value){...}) multiple times can indeed make code cleaner... specially if each one could set its own invalidMessage('xx')... hint,hint... ;-)

ManuelDeLeon commented 8 years ago

That would explode the API. Every validation would then have to support adding a valid/invalid message. I think the validation API is already bloated so I'll leave it as it is for now.

ManuelDeLeon commented 8 years ago

Validations are in 4.1.0: https://viewmodel.org/docs/viewmodels#validation

And with that, I close this ticket.

comerc commented 8 years ago

Template2 released!

key features

  • compatible with Blaze.Template
  • minimum changes for migration your great project to template2
  • one time declaration of variables to model - inside input fields attribute
  • validate input data and get form data without excess coding
  • support of model type as SimpleSchema
  • may be extended for support any other model type (Astronomy etc.)
head
  title simple

body
  +demo(test='123')

template(name="demo")
  div props: 
    = props.test
  div 
    a.node(href="#") NEW
  form
    input(type="text" value-bind="fieldHead|throttle:500") 
    = state.fieldHead
    input(type="text" value-bind="fieldBody|debounce:500") 
    = state.fieldBody
    button(type="submit") SUBMIT
    button#reset RESET
    = state.submitMessage
  = state.errorMessages
  ul
    +each node in nodes
      li 
        a.node(href="#" data-node-id=node._id)  
          = node.fieldHead  
          | / 
          = node.fieldBody
Template.demo.onCreated ->
  @propsSchema new SimpleSchema(test: type: String)
  @modelSchema Nodes.simpleSchema()
  @states
    nodeId: false
    submitMessage: ''
  @helpers
    nodes: ->
      Nodes.find()
  @events
    'click a.node': (e) ->
      e.preventDefault()
      @state.nodeId = $(e.target).data('node-id') or false
    'submit form': (e) ->
      e.preventDefault()
      @viewDoc (error, doc) ->
        return if error
        # save data
        if @state.nodeId
          Nodes.update @state.nodeId, $set: doc,
            => @state.submitMessage = 'updated'
        else
          @state.nodeId = Nodes.insert doc,
            => @state.submitMessage = 'inserted'

# old school of declaration with context of Template.instance()
Template.demo.eventsByInstance
  'click #reset': (e) ->
    e.preventDefault()
    @modelDoc false
    @state.submitMessage = ''

Template.demo.onRendered ->
  @modelMap() # magic here :)
  @autorun =>
    if @state.nodeId
      @modelDoc Nodes.findOne @state.nodeId
    else
      @modelDoc false
    @state.submitMessage = ''
@Nodes = new (Mongo.Collection)('nodes')

@NodeSchema = new SimpleSchema
  fieldHead:
    type: String
    label: 'My Head'
    max: 3
    defaultValue: '111'
  fieldBody:
    type: String
    label: 'My Body'
    min: 3
    defaultValue: '777'

Nodes.attachSchema(NodeSchema)