Expectations for JSX Support

As mentioned on the 9/1/17 DoneJS Contributor’s Meeting, we are starting to think about how to support JSX in CanJS. If you use / like / know about JSX, I’d like your ideas for what expectations you have when working with JSX.

If we said tomorrow that “You can now use JSX with CanJS”, what would you automatically expect to work?

Some made up examples to get people thinking about this:

I would expect to be able to use JSX to render some properties from a DefineMap like:

import DefineMap from 'can-define/map/map';
import { jsx } from 'can-jsx';

var data = new DefineMap({
  name: 'Adam'
});

document.body.appendChild(
  jsx(<h1>{data.name}</h1>)
)

I would expect to be able to use JSX with a component like

import Component from 'can-component';
import DefineMap from 'can-define/map/map';
import { jsx } from 'can-jsx';

const VM = DefineMap.extend({
  greeting: { type: 'string', value: 'Hello' }
});

Component.extend({
  tag: 'my-cool-component',
  ViewModel: VM,
  view: jsx(
    <h1>{ greeting } World!</h1>
  )
});

I would expect to be able to use can-stache-bindings within my JSX

I would expect to be able to use can-components in JSX

I would expect JSX to only support Custom Elements

I would expect to be able to use React/Preact lifecycle methods

I would expect JSX to magically write my app for me

Again, these are all hypothetical, made up examples. Please post your own ideas for the expectations you have when working with JSX in the comments. Sample code is good, but it doesn’t have to be totally thought out. No finalized APIs will be made from this post. Feel free to post code from a non-CanJS app that uses JSX to show the kind of things you do with JSX – or no code at all, just post your thoughts.

Thanks!

1 Like

Concepts in CanJS that don’t map to JSX

two-way binding vs 1 way binding

There really is no concept of binding in JSX right, you are just assigning dynamic attributes

return <MyComponent someAttr={ somethingFromThisFunctionsScope } />

Would we default to 1-way parent to child? Or 2-way binding?

return <MyComponent numRemaining={ viewModel.count } />

Event binding

Related to the last point, do we embrace the idea of passing callbacks into components for observations (not really the can-js way), or do we introduce some sort of event binding? Do we listen for bubbled events? What about capture phase events?

return ([
  <MyComponent onBubbledEvent={ vm.handleBubbled } />
  <MyComponent onEventCapture={ vm.handleCaptured } />
])

Binding to sibling components attrs

<drivers-licenses {^selected}="*selectedDriver"/>
<edit-driver {driver}="*selectedDriver"/>

What would this look like with JSX?

Child to parent binding

<my-component childProp:to="value"/>

What does that look like? Event binding? Callbacks?

Other things to think about

Higher Order Components

Do our JSX components create custom-elements? Or are they like React components that are just functions that may or may not return DOM-generating vdom?

connectViewModel(ViewModel)(props => (
  <div className={ props.type }>{ props.message }</div>
))

If we choose the latter, we would be able to embrace the interesting idea of HoCs, but if not, do we only do “dashed” components?

return ([
  <my-component onClick={ vm.toggleActive } />
  <something-else onInput={ ev => vm.setTitle(ev.target.value) } />
])

View-Models

I really like the idea of the view model not knowing anything about DOM. It would be nice if all the messy DOM and events stuff was limited to a “view”.

class MyComponent extends can.JSXComponent {
  updateText(ev) {
    this.viewModel.updateText(ev.target.value);
  }
  handleKeydown(ev) {
    if (event.which == 13 || event.keyCode == 13) {
      ev.preventDefault();
			this.viewModel.editing = false;
    }
  }
	toggleEdit() {
		this.isEditing = !this.isEditing;
	}
  render() {
    if (this.isEditing) {
      return <input onInput={ this.updateText }
			              onKeydown={ this.handleKeyDown }
									  value={ this.viewModel.text } />
    }
    return <span onDoubleClick={ this.toggleEdit }>{this.viewModel.text}</span>
  }
}

So should our “views” be some sort of class with it’s own DOM state, and lifecycle, and the ability to hold some form of state? At that point are we just recreating React?

Or should we just use JSX in our view/template function? If then, are we losing some of the benefits of JSX?

const ViewModel = DefineMap(/*...*/);

class ThingyModal extends can.JSXComponent {
  tag = 'thingy-modal';
  leakScope = false;
  events = {
    'body click'(ev) {
      if (this.viewModel.isOpen && !ev.target.contains(this.element)) {
        this.viewModel.isOpen = false
      }
    }
  };
  ViewModel() {
    return new ViewModel();
  }
  view() {
    return (
      <bit-modal showModal={this.viewModel.isOpen}>
        <h1>Thingy</h1>
        <img src="./thingy" />
        <p>Some words about the thingy</p>
      </bit-modal>
    );
  }
}

…to me there is absolutely nothing compelling about using JSX in this example or Kevins super simple examples. We’d be better off sticking with stash.

Are we talking specifically about Spec the https://facebook.github.io/jsx/ ?
…or how it’s used in React?

What is the real appeal of JSX?

On thing for me is the idea that I don’t have to do anything template specific for conditionals, and if/else and comparisons.

If I can make it work in JavaScript I “know” it will work in JSX.

For example this simple and easy to understand bit of JSX

{ a === b && c === d ? (
    <div>Compatible</div>
) : (
    <div class="warn">Incompatible</div>
)}

Becomes more complicated in stache

{{#is a b }}
  {{#is c d }}
    <div>Compatible</div>
  {{else}}
    <div class="warn">Incompatible</div>
  {{/is}}
{{else}}
  <div class="warn">Incompatible</div>
{{/is}}

…and pretty much encourages you to make a helper or a function expression from your scope to handle the logic, or you end up repeating yourself.

This is a simple example, but as complexity grows, as do the troubles with the template abstraction.

Personally, I find this easier to understand:

const pending = tasks.filter(t => t.status === 'pending');
const closed = tasks.filter(t => t.status === 'closed');
const Task = ({ task }) => (
  <div className={`task ${ task.status }`}>
    { task.description }
    <footer>{task.deadline}</footer>
  </div>
);
return <div className="tasks">
  <h3>Pending</h3>
  { pending.map(t => <Task task={t} />) }
  <h3>Closed</h3>
  { closed.map(t => <Task task={t} />) }
</div>

…than this…

{{<Task}}
  <div class="task {{this.status}}"}>
    {{ this.description }}
    <footer>{{ this.deadline }}</footer>
  </div>
{{/Task}}
<div class="tasks">
  <h3>Pending</h3>
  {{#tasks}}
    {{#eq status 'pending'}}
      {{>Task}}
    {{/eq}}
  {{/tasks}}
  <h3>Closed</h3>
  {{#tasks}}
    {{#eq status 'closed'}}
      {{>Task}}
    {{/eq}}
  {{/tasks}}
</div>

…because it looks like JavaScript (and is) and not some special templating language I need to know the secret sauce for.

So… yeah…

I didn’t answer anything, just slogging some ideas around to think about.

I don’t know if this is this is the right framing. At the point we start customizing jsx, I think we should really be looking to make it work with the best of our tech, JSX idioms be damned. I think this was a problem with stache. We added in the worst of mustache (implicit scope walking) and handlebars (weird calling of functions) instead of designing the best fit for what we wanted. I don’t think we should rely on jsx bindings (which don’t support two-way way bindings anyway).

I think that’s a good point and we don’t want to make the same mistakes with JSX, but on the other hand, I also think it would be a mistake to say we “support JSX” and not actually support anything that people expect to work when they’re using JSX.

This discussion isn’t making any final decisions and I still think it’s useful information to know what people like about JSX and what they expect to work when they’re working with JSX.