Full styleguide now available via yarn styleguide

Due to a number of hacks, the style guide can be used to show Backbone
views. This will allow a smooth path from the old way of doing things to
the new.
This commit is contained in:
Scott Nonnenberg 2018-04-03 15:56:12 -07:00
parent 893fb1cb9e
commit 1326b26585
No known key found for this signature in database
GPG key ID: 5F82280C35134661
21 changed files with 4006 additions and 363 deletions

View file

@ -0,0 +1,6 @@
```jsx
<util.MessageParents theme="android">
<Message />
</util.MessageParents>
```

View file

@ -0,0 +1,36 @@
import React from 'react';
/**
* A placeholder Message component, giving the structure of a plain message with none of
* the dynamic functionality. We can build off of this going forward.
*/
export class Message extends React.Component<{}, {}> {
public render() {
return (
<li className="entry outgoing sent delivered">
<span className="avatar" />
<div className="bubble">
<div className="sender" dir="auto" />
<div className="attachments" />
<p className="content" dir="auto">
<span className="body">
Hi there. How are you doing? Feeling pretty good? Awesome.
</span>
</p>
<div className="meta">
<span
className="timestamp"
data-timestamp="1522800995425"
title="Tue, Apr 3, 2018 5:16 PM"
>
1 minute ago
</span>
<span className="status hide" />
<span className="timer" />
</div>
</div>
</li>
);
}
}

View file

@ -0,0 +1,2 @@
This is Reply.md.

View file

@ -0,0 +1,14 @@
import React from 'react';
interface IProps { name: string; }
interface IState { count: number; }
export class Reply extends React.Component<IProps, IState> {
public render() {
return (
<div>Placeholder</div>
);
}
}

View file

@ -1,38 +0,0 @@
import React from 'react';
interface IProps { name: string; }
interface IState { count: number; }
const items = [
'one',
'two',
'three',
'four',
];
export class InlineReply extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
count: 0,
};
}
public render() {
const { name } = this.props;
return (
<div>
This is a basic component. Hi there, {name}!
</div>
);
}
}
export function greeter2(person: any) {
// console.log(items);
return `Hello, ${person}`;
}

View file

@ -1,7 +0,0 @@
import { InlineReply } from './sub/test2';
// console.log(InlineReply);
export function greeter(person: any) {
return 'Hello, ' + person;
}

View file

@ -0,0 +1,20 @@
Rendering a real `Whisper.MessageView` using `<util.MessageParents />` and
`<util.BackboneWrapper />`.
```jsx
const model = new Whisper.Message({
type: 'outgoing',
body: 'text',
sent_at: Date.now() - 5000,
})
const View = Whisper.MessageView;
const options = {
model,
};
<util.MessageParents theme="android">
<util.BackboneWrapper
View={View}
options={options}
/>
</util.MessageParents>
```

View file

@ -0,0 +1,78 @@
import React from 'react';
interface IProps {
/** The View class, which will be instantiated then treated like a Backbone View */
readonly View: IBackboneViewConstructor;
/** Options to be passed along to the view when constructed */
readonly options: object;
}
interface IBackboneView {
remove: () => void;
render: () => void;
el: HTMLElement;
}
interface IBackboneViewConstructor {
new (options: object): IBackboneView;
}
/**
* Allows Backbone Views to be rendered inside of React (primarily for the styleguide)
* while we slowly replace the internals of a given Backbone view with React.
*/
export class BackboneWrapper extends React.Component<IProps, {}> {
protected el: Element | null;
protected view: IBackboneView | null;
protected setEl: (element: HTMLDivElement | null) => void;
constructor(props: IProps) {
super(props);
this.el = null;
this.view = null;
this.setEl = (element: HTMLDivElement | null) => {
this.el = element;
this.setup();
};
this.setup = this.setup.bind(this);
}
public setup() {
const { el } = this;
const { View, options } = this.props;
if (!el) {
return;
}
this.view = new View(options);
this.view.render();
// It's important to let the view create its own root DOM element. This ensures that
// its tagName property actually takes effect.
el.appendChild(this.view.el);
}
public teardown() {
if (!this.view) {
return;
}
this.view.remove();
this.view = null;
}
public componentWillUnmount() {
this.teardown();
}
public shouldComponentUpdate() {
// we're handling all updates manually
return false;
}
public render() {
return <div ref={this.setEl} />;
}
}

View file

@ -0,0 +1,8 @@
The simplest example of using the `<MessagesParents />` component:
```jsx
<util.MessageParents theme="android">
<div>Just a plain bit of text</div>
</util.MessageParents>
```

View file

@ -0,0 +1,31 @@
import React from 'react';
interface IProps {
/**
* Corresponds to the theme setting in the app, and the class added to the root element.
*/
theme: "ios" | "android" | "android-dark";
}
/**
* Provides the parent elements necessary to allow the main Signal Desktop stylesheet to
* apply (with no changes) to messages in this context.
*/
export class MessageParents extends React.Component<IProps, {}> {
public render() {
const { theme } = this.props;
return (
<div className={theme}>
<div className="conversation">
<div className="discussion-container" style={{padding: '0.5em'}}>
<ul className="message-list">
{this.props.children}
</ul>
</div>
</div>
</div>
);
}
}

24
js/react/util/index.ts Normal file
View file

@ -0,0 +1,24 @@
// Helper components used in the styleguide, exposed at 'util' in the global scope via the
// context option in reaat-styleguidist.
export { MessageParents } from './MessageParents';
export { BackboneWrapper } from './BackboneWrapper';
// Here we can make things inside Webpack available to Backbone views like preload.js.
import React from 'react';
import ReactDOM from 'react-dom';
import { Message } from '../conversation/Message';
import { Reply } from '../conversation/Reply';
// Required, or TypeScript complains about adding keys to window
const parent = window as any;
parent.React = React;
parent.ReactDOM = ReactDOM;
const SignalReact = parent.Signal.React = parent.Signal.React || {};
SignalReact.Message = Message;
SignalReact.Reply = Reply;

View file

@ -0,0 +1,47 @@
/* global Backbone: false */
// Additional globals used:
// window.React
// window.ReactDOM
// window.i18n
// eslint-disable-next-line func-names
(function () {
'use strict';
window.Whisper = window.Whisper || {};
window.Whisper.ReactWrapper = Backbone.View.extend({
className: 'react-wrapper',
initialize(options) {
const { Component, props, onClose } = options;
this.render();
this.Component = Component;
this.onClose = onClose;
this.update(props);
},
update(props) {
const updatedProps = this.augmentProps(props);
const element = window.React.createElement(this.Component, updatedProps);
window.ReactDOM.render(element, this.el);
},
augmentProps(props) {
return Object.assign({}, props, {
close: () => {
if (this.onClose) {
this.onClose();
return;
}
this.remove();
},
i18n: window.i18n,
});
},
remove() {
window.ReactDOM.unmountComponentAtNode(this.el);
Backbone.View.prototype.remove.call(this);
},
});
}());