gcanti / tcomb-form

Forms library for react
https://gcanti.github.io/tcomb-form
MIT License
1.16k stars 136 forks source link

Custom list and list item template in a table #219

Closed johnraz closed 8 years ago

johnraz commented 8 years ago

Hi,

I'm trying to achieve the following look and feel for my list:

+----------+----------+----------------+
| Pet Name | Pet Type |    Actions     |
+----------+----------+----------------+
| Foo      | Bar      | remove/up/down |
| Foo2     | Bar2     | remove/up/down |
+----------+----------+----------------+
| Add                                  |
+----------+----------+----------------+

After following the guidance here #109, I ended up with the template shown at the bottom of this issue.

The problem with that template is that the petLayout

var petLayout = function(locals){
  return (
      <div>
        <td>{locals.inputs.name}</td>
        <td>{locals.inputs.type}</td>
      </div>
  );
};

is breaking the dom (because the <div> is a child of a <tr>) and hence react raises an Invariant Violation error due to the dom being auto-corrected by the browser (tried in chrome).

Using a <tr> tag instead of the <div> breaks too.

I've been able to go around this with the following:

var petLayout = function(locals){
  return (
      <td>
        {locals.inputs.name}
        <td>{locals.inputs.type}</td>
      </td>
  );
};

It kinda does work "by chance" because the way the browser is auto-correcting the dom suits react ... But it is very fragile and I'd like to find another way.

I could ditch the <table> and try to recreate the same design with divs but well, the rest of the application is using tables for such components and I'd like to keep it simple.

Thanks again for the support !

import React from 'react/addons';
import t from 'tcomb-form';

var Form = t.form.Form;

var Animal = t.enums({
 dog: "Dog",
 cat: "Cat"
});

var Pet = t.struct({
  name: t.Str,
  type: Animal
});

var Person = t.struct({
  pets: t.list(Pet)
});

var listLayout = function(locals){
  return (
      <table className="table table-bordered table-responsive table-striped">
        <thead>
            <tr>
                <th>Pet name</th>
                <th>Pet type</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
        {
          locals.items.map(function (item) {
            return (
                <tr key={item.key}>
                    {item.input}
                    <td>
                        {
                            item.buttons.map(function (button, i) {
                                return <button key={i} className="btn btn-info" onClick={button.click}>{button.label}</button>;
                            })
                        }
                    </td>
                </tr>
            )
          })
        }
        <tr>
            <td colSpan="3">
                <button className="btn btn-info" onClick={locals.add.click}>{locals.add.label}</button>
            </td>
        </tr>
        </tbody>
    </table>
  );
};

var petLayout = function(locals){
  return (
      <div>
        <td>{locals.inputs.name}</td>
        <td>{locals.inputs.type}</td>
      </div>
  );
};

var options = {
  fields: {
    pets: {
      template: listLayout,
      item: {
        template: petLayout
      }
    }
  }
};
gcanti commented 8 years ago

Hi, Yeah tables are a pain. One thing I don't understand. You have this template

var petLayout = function(locals) {
  return (
    <span>
      <td>{locals.inputs.name}</td>
      <td>{locals.inputs.type}</td>
    </span>
  );
};

but the model is

var Pet = t.struct({
  type: Animal
});

What's locals.inputs.name ?

johnraz commented 8 years ago

Hey @gcanti , Silly mistake on my side ;-)

I updated the initial issue's content I removed name from Person and moved it where it belongs:

var Pet = t.struct({
  name: t.Str,
  type: Animal
});
johnraz commented 8 years ago

I was thinking maybe I could simply override the field template instead of the pet template ?

Does that sound as a viable option ?

gcanti commented 8 years ago

I just can't find a workaround for React's limitation in returning only one node from a render method :-(

var petLayout = function(locals) {
  return (
    <div> // <= ouch!
      <td>{locals.inputs.name}</td>
      <td>{locals.inputs.type}</td>
    </div>
  );
};
johnraz commented 8 years ago

Yep, same here :/

So I guess I will have to deal with a field level template then. I'll let you know how it turn out

VinSpee commented 8 years ago

won't doing this work?

var petLayout = function(locals) {
  return ([
      <td>{locals.inputs.name}</td>,
      <td>{locals.inputs.type}</td>
  ]);
};
johnraz commented 8 years ago

@VinSpee sadly it will not work no, react still complains with Adjacent JSX elements must be wrapped in an enclosing tag

gcanti commented 8 years ago

Hi @johnraz,

I know it's probably too late, but I found an interesting hack:

const Animal = t.enums({
  dog: 'Dog',
  cat: 'Cat'
})

const Pet = t.struct({
  name: t.Str,
  type: Animal
})

const Person = t.struct({
  pets: t.list(Pet)
})

const listLayout = locals => {
  return (
    <table className="table table-bordered table-responsive table-striped">
      <thead>
        <tr>
          <th>Pet name</th>
          <th>Pet type</th>
          <th>Actions</th>
        </tr>
      </thead>
      <tbody>
      {
        locals.items.map((item) => {
          //
          // here's the trick: setting the template on the fly and clone the item!
          //
          const options = {
            template: (sublocals) => {
              return (
                <tr key={item.key}>
                  <td>{sublocals.inputs.name}</td>
                  <td>{sublocals.inputs.type}</td>
                  <td>
                    {
                      item.buttons.map((button, i) => {
                        return <button key={i} className="btn btn-info" onClick={button.click}>{button.label}</button>
                      })
                    }
                  </td>
                </tr>
              )
            }
          }

          return React.cloneElement(item.input, {
            key: item.key,
            options: options
          })
        })
      }
      <tr>
        <td colSpan="3">
          <button className="btn btn-info" onClick={locals.add.click}>{locals.add.label}</button>
        </td>
      </tr>
      </tbody>
    </table>
  )
}

const options = {
  fields: {
    pets: {
      template: listLayout
    }
  }
}

It's somehow horrible, but seems to work. Probably it's too risky for production code though