Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Support JSX #3569

Open
phillipskevin opened this Issue Sep 21, 2017 · 0 comments

Comments

Projects
None yet
1 participant
@phillipskevin
Copy link
Collaborator

phillipskevin commented Sep 21, 2017

TLDR:

JSX allows you to

  • use JavaScript expressions directly in templates
  • easily reuse pieces of your template by extracting out functions and variables
  • debug templates using the devtools you already know

Combining JSX with CanJS will also allow

  • templates to be updated when observables change without requiring setState or shouldComponentUpdate
  • the use of simple and intuitive event, one-way, and two-way bindings
  • support for adding virtual properties (like isResolved and isPending on promises)
  • performant DOM updates without needing to diff the entire template

This was discussed on a recent live stream (25:10), a previous live stream (9:38), and a contributors meeting (21:16).

Full Proposal

This proposal is to add support for using JSX to CanJS, so it can be used alongside or in place of can-stache.

Adding support for JSX benefits CanJS users because JSX is very different from can-stache and other HTML templating languages.

It allows you to use JavaScript expressions directly in your templates - you don't need to learn new syntax to do loops, if statements, or text formatting.

<nav>
  {isLoggedIn ? (
    <a href="/login">Log In</a>
  ) : (
    Hello, {user.firstName}!
  )}
</nav>

You can easily reuse pieces of your template by extracting out functions or variables. Again, no need to learn new syntax for creating and rendering partials or calling helpers.

  const sidebar = (
    <ul>
      {props.posts.map((post) =>
        <li key={post.id}>
          {post.title}
        </li>
      )}
    </ul>
  );
  const content = props.posts.map((post) =>
    <div key={post.id}>
      <h3>{post.title}</h3>
      <p>{post.content}</p>
    </div>
  );
  return (
    <div>
      {sidebar}
      <hr />
      {content}
    </div>
  );

JSX transpiles to function calls, which means you can debug it using the devtools you already know. Some people even like using this functional syntax directly.

For example

return (
  <div>
    <p>Hello, {greeting}!</p>
  </div>
);

becomes:

return h("div", null,
  h("p", null, "Hello, ", greeting, "!" )
);

And while people love JSX, combining it with the power of CanJS would allow us to add exciting features on top of what JSX gives:

  • Simple and intuitive event, one-way, and two-way bindings
  • Support for adding virtual properties (like isResolved and isPending on promises)
  • Enabling performant DOM updates without DOM-diffing

Example

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

var WeatherData = DefineMap.extend({
    // ...
});

var view = function(data) {
	const toClassName = text => text.toLowerCase().replace(/ /g, "-");
	
	return render(() =>
        <div class="weather-widget">
            <div class="location-entry">
                <label for="location">Enter Your location:</label>
                <input id="location" value:to="vm.location" type="text"/>
            </div>

            { placesPromise.isPending &&
	            <p class="loading-message">
	            	Loading places…
	            </p>
            }

            { data.showPlacePicker &&
	            <div class="location-options">
	                <label>Pick your place:</label>
	                <ul>
	                    { places.map(place =>
	                    	<li on:click="data.pickPlace(place)">{place.name}</li>
	                    )}
	                </ul>
	            </div>
            }

            { data.place &&
	            <div class="forecast">
	                <h1>10 day {data.place.name} Weather Forecast</h1>
	                <ul>
	                    {data.place.forecasts.map(forecast =>
		                    <li>
		                        <span class='date'>{forecast.date}</span>
		                        <span class={`description ${toClassName(forecast.text)}`}>{forecast.text}</span>
		                        <span class='high-temp'>{forecast.high}<sup>&deg;</sup></span>
		                        <span class='low-temp'>{forecast.low}<sup>&deg;</sup></span>
		                    </li>
	                    )}
	                </ul>
	            </div>
            }
        </div>
    );
}

document.body.appendChild(
    view(new WeatherData())
);

Technical Details

There are a few steps to making JSX possible in a CanJS app that will be discussed in the following sections:

  1. Import the h and render functions from can-jsx
  2. Include the babel plugin that will transpile JSX into hyperscript functions (this is the h function imported in #1)
  3. Call the render function passing it a function that returns a JSX Node1

The rest of this proposal will explain the technical details of how these 3 steps work.

Imports

import { h, render } from 'can-jsx';

As mentioned above, you will use the render function directly. The h function will be used by called by the code that is transpiled from JSX.

h

The h function will create "virtual DOM nodes". Virtual DOM just means objects. For example,

h('div', { class: 'red' }, [ 'Hello, World!' ])

will create an object that looks like this:

{
  "nodeName": "div",
  "attributes": {
    "class": "red"
  },
  "children": [
    {
      "nodeName": "text",
      "value": "Hello, World!"
    }
  ]
}

render

The render function will turn a virtual DOM object into a DOM Element and set up live binding. It will be responsible for listening to changes in the CanJS observables used within the render function, rebuilding the virtual DOM object, diffing the changes from the previous render, and updating the DOM to match.

This will look something like this:

module.exports = function(renderer) {
	var vnode = renderer();
	var el = vnodeToNode(vnode);

	var obs = new Observation(renderer);

	canReflect.onValue(obs, function(newVnode) {
		var patch = diff(vnode, newVnode);
		applyDiff(el, patch);
		vnode = newVnode;
	});

	return el;
};

Live attributes

In order to provide live-bindings similar to can-stache-bindings, we will transpile attributes using :to, :from, and :bind to functions that will set up these one-way or two-way bindings. For example, instead of this

<input id="location" value:to="vm.location" type="text"/>

transpiling to

h('input', {
	id: 'location',
	value: vm.location,
	type: 'text' 
})

it will transpile to something like2 this

h('input', {
	id: 'location',
	value: bindTo(vm, 'location'),
	type: 'text' 
})

Virtual properties

Properties like isPending and isResolved will be made available like they are today in can-stache. This will allow you to easily work with Promises, Streams, and other data types.

{ placesPromise.isPending &&
    <p class="loading-message">
    	Loading places…
    </p>
}

Virtual properties will be handled similar to bindings. This will be transpiled to something like3

get(placesPromise, 'isPending') &&
h(
  'p',
  { 'class': 'loading-message' },
  'Loading places\u2026'
)

This get function will use canReflect.getKeyValue to read virtual properties off of an observable.

Event Handling

Event handlers can be set up using normal JSX like

	function handleClick(e) {
		e.preventDefault();
		console.log('The link was clicked.');
	}

	return (
		<a href="#" onClick={handleClick}>
	);

This will use addEventListener to set up event handlers and will use removed events to safely clean up the event handlers.

You can also use on: bindings in order to create event listeners. This will allow you to easily pass arguments to functions without having to use arrow functions which can cause problems with diffing.

This code

<li on:click="data.pickPlace(place)">{place.name}</li>

will transpile to something like

  return h(
    'li',
    { onClick: getBoundFunction(data, pickPlace, [place]) },
    place.name
  )

which might be implemented something like

var boundFunctionCache = new WeakMap();
var getBoundFunction = function(context, methodName, args){
        var contextCache = weakCache.get(context);
        var res = contextCache && contextCache[methodName]
        if(!res) {
                res = function() {
                        context[methodName].apply(context, args);
                };
                weakCache.set( context, {[methodName]: res } )
        }
        return res;
}

This will allow the context and arguments to be set correctly without creating a new function every time render is called, which will prevent the diff from modifying more than is necessary.

Using a WeakMap will also allow this to be done in a memory-safe way.

Iterating over Lists

<ul>
    { places.map(place =>
    	<li on:click="data.pickPlace(place)">{place.name}</li>
    )}
</ul>

Iterating over lists will work out-of-the-box with whatever iteration method you want to use since any change to the output will be caught by the diff.

To maximize performance when you need it we will also create an each helper that will set up a can-view-live.list in order for minimal updates to be made when when changes are made to the list or items in the list.

This might look like:

import { each } from 'can-jsx/helpers';

...

<ul>
  {each(numbers, (number) =>
    <li>{number}</li>
  )}
</ul>

This will also prevent the <li>s in this tree from being diffed when changes occur.

Footnotes

1: a function is passed to the render function so that you can perform logic and have it be observable. For example, if you want to do something like:

return render(() => {
	const count = vm.count;
	
	return <div>{count}</div>;
});

If you instead did count = vm.count outside of the render function, the UI would not update when vm.count changes.

2: in order for these functions to be in scope, it might be more like

value: render.bindTo(vm, 'location')

Or perhaps, we will also import bindings from can-jsx if they want to use them like

import { h, render, bindings } from 'can-jsx';

and then transpile to

value: bindings.bindTo(vm, 'location')

3: again, not exactly sure where the get function will be in order for it to be in scope. Maybe it will just be render.get.

@phillipskevin phillipskevin added the Epic label Sep 21, 2017

@chasenlehara chasenlehara changed the title Support JSX-like Template Syntax Support JSX-like template syntax Sep 22, 2017

@justinbmeyer justinbmeyer referenced this issue Nov 2, 2017

Closed

Epoch 1 Survey Questions #77

24 of 24 tasks complete

@phillipskevin phillipskevin changed the title Support JSX-like template syntax Proposal: Support JSX Jan 26, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.