frint-react
React package for Frint
Guide
Installation
With npm:
$ npm install --save react react-dom prop-types frint-react
Via unpkg CDN:
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.4.0/Rx.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react-dom.min.js"></script>
<script src="https://unpkg.com/prop-types@15.5.10/prop-types.js"></script>
<script src="https://unpkg.com/frint@latest/dist/frint.min.js"></script>
<script src="https://unpkg.com/frint-react@latest/dist/frint-react.min.js"></script>
<script>
</script>
Terminologies
React:
Reactive programming:
Observable
: As detailed in the spec, and implemented in RxJS.
Frint:
Region
: Component that can be embedded anywhere
Usage
We start by importing the necessary functions from the library:
import React from 'react';
import { createApp } from 'frint';
import { render } from 'frint-react';
Now let's create our first Component:
function Root() {
return (
<div>
<p>Hello World</p>
</div>
);
}
Now we need to create our Root App, and assign the previously defined Component as our root component for the App:
const RootApp = createApp({
name: 'MyRootApp',
providers: [
{
name: 'component',
useValue: Root
}
]
});
Now that we have everything ready, we can instantiate our app, and render it:
window.app = new RootApp();
render(window.app, document.getElementById('root'));
The code above asumes your page has an element with an id root
:
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
</body>
</html>
Regions
The library already ships with a Region
component, and a RegionService
.
We use the concept of regions for defining areas in our Components, where other Apps can load themselves in.
For example, imagine the Root component of our Root App above, we can define a Region named sidebar
as follows:
import React from 'react';
import { Region } from 'frint-react';
function Root() {
return (
<div>
<p>Hello World from Root App!</p>
<Region name="sidebar" />
</div>
);
}
That's just defining the Region only. Let's now create an App, and assign it to the sidebar
region:
import { createApp } from 'frint';
function AppComponent() {
return (
<p>I am App</p>
);
}
const App = createApp({
name: 'MyApp',
providers: [
{
name: 'component',
useValue: AppComponent,
}
],
});
Now that we have our App defined, we can register it to our Root App:
window.app.registerApp(App, {
regions: ['sidebar'],
weight: 10,
});
Now when you refresh your browser, you would notice your App being rendered inside the Region sidebar
.
Region and data
It is possible that when defining the Region with a name, you would also want to pass some data to it, so that whenever an App gets rendered inside it, the App would be able to access that data.
From the above example of sidebar
Region, imagine us passing some data too via props:
function Root() {
const data = {
foo: 'bar'
};
return (
<div>
<p>Hello World from Root App!</p>
<Region name="sidebar" data={data} />
</div>
);
}
That's only the Region
component's implementation part. How do we access it from our App now?
Enter RegionService
. This is a Service that we can pass in our App's providers list, allowing us to later have access to Region's props.
import { RegionService } from 'frint-react';
const App = createApp({
name: 'MyApp',
providers: [
{
name: 'component',
useValue: AppComponent,
},
{
name: 'region',
useClass: RegionService,
},
],
});
Once your App is registered and rendered, you can get access to your App instance, which can then allow you to deal with Region's props:
const myApp = window.app.getAppInstance('MyApp');
const region = myApp.get('region');
const regionData$ = region.getData$();
regionData$.subscribe((data) => {
console.log(data);
});
We will discuss more in details how to get Region's props in your App's components via observe
in the next section.
Observing components
We encourage everyone to write their components in as dumb way as possible. Meaning, we just pass the props, and Components just render them. Nothing more.
But real-world applications are complex, and data can come from anywhere, at any time, asynchronously.
Enter observe
. This is a function that we ship with the library for making your Components reactive.
A very simple example would be:
function MyComponent(props) {
return (
<p>Interval: {props.interval}</p>
);
}
We just created a component, that prints out a prop called interval
. Nothing fancy there. But we wish the interval to update itself every second. Instead of handling it from within the Component, we can do it with observe
as follows:
import { Observable } from 'rxjs';
import { observe } from 'frint-react';
const MyObservedComponent = observe(function () {
return Observable
.interval(1000)
.map(x => ({ interval: x }));
})(MyComponent);
We have just made our simple Component reactive, by wrapping it with observe
. Now it will continue to update the interval
prop every second until the Component has unmounted itself.
Observing Region's data
In previous example, we showed you how to access Region's data via RegionService
. Now let's see how we can pass it to your App's component too:
const ObservedAppComponent = observe(function (app) {
const interval$ = Observable
.interval(1000)
.map(x => ({ interval: x }));
const region = app.get('region');
const regionData$ = region.getData$()
.map(regionData => ({ regionData: regionData }));
return interval$
.merge(regionData$)
.scan((props, emitted) => {
return {
...props,
...emitted,
};
}, {});
})(AppComponent);
When your App's component renders, latest props will be passed to it in this structure:
{
interval: 1,
regionData: {
foo: 'bar'
}
}
Helper function for streaming props
As the number of observables grow, it might be difficult to maintain your observe
implementation. That's why we are also shipping a streamProps
function in the library to make it easier for you:
import { streamProps } from 'frint-react';
const MyObservedComponent = observe(function (app) {
return streamProps({})
.set(
Observable.interval(1000),
x => ({ interval: x }),
)
.set(
app.get('region').getData$(),
regionData => ({ regionData })
)
.set({
key: 'value'
})
.set('myKey', 'myValue')
.get$();
})(AppComponent);
The props available inside your Component would then be in this format:
{
interval: 1,
regionData: { foo: 'bar' },
key: 'value',
myKey: 'myValue'
}
Multi-instance Apps
This is a use case where you have multiple instances of Region with the same name mounted in the DOM. And the apps rendered in them should have their own independent scoped instances too.
Think of a scenario where you have a TodoList, and you want a Region defined for each Todo item:
function MyComponent() {
const todos = [
{ id: '1', title: 'First todo' },
{ id: '2', title: 'Second todo' },
{ id: '3', title: 'Third todo' },
];
return (
<ul>
{todos.map((todo) => {
return (
<li>
<h3>{todo.title}</h3>
<Region
name="todo-item"
data={{ todo }}
uniqueKey={`todo-item-${todo.id}`}
/>
</li>
);
})}
</ul>
);
}
Now we may have an App that we want to be rendered in todo-item
Regions.
Let's create an App, that will receive the todo
object, and render the title in UPPERCASE format.
import React from 'react';
import { createApp } from 'frint';
import { observe, RegionService } from 'frint-react';
function AppComponent(props) {
const { todo } = props;
return (
<p>Todo in upper case: {todo.title.toUpperCase()}</p>
);
}
const ObservedAppComponent = observe(function (app) {
return streamProps()
.set(
app.get('region').getData$(),
data => ({ todo: data.todo })
)
.get$();
})(AppComponent);
const App = createApp({
name: 'MyApp',
providers: [
{
name: 'component',
useValue: ObservedAppComponent
},
{
name: 'region',
useClass: RegionService
}
]
});
Now comes the part of registering our App as a multi-instance app:
window.app.registerApp(App, {
regions: ['todo-item'],
multi: true,
});
API
render
render(app, node)
Renders a Root App in target DOM node.
Arguments
app
(App
): The Root App instance.node
(Element
): The DOM Element where you want your App to render.
observe
observe(fn)(MyComponent)
Arguments
fn
(Function
): The function returning an Observable.
- The
fn
accepts app
as an argument, which is the instance of your Root App or the App in scope - It should return an
Observable
Returns
Function
: that can be called with a Component to return an observed Component ready for embedding and rendering anywhere.
Region
Region
The Region component.
Props
name
(String
): The name of the Regiondata
(Object
): Data to be made available to rendered AppuniqueKey
(String
[optional]): Used when there are multiple Regions of the same name
. It prop must be unique and fixed throughout the whole app.
RegionService
RegionService
If your App wishes to receive data coming from the Region component it rendered in, RegionService is your way to access it.
Methods exposed by the instance:
emit
emit(props)
The props that need to be emitted (Region component uses it internally).
Arguments
props
(Object
)
Returns
void
.
getProps$
getProps$()
Returns
Observable
: of emitted props from the Region component.
getData$
getdata$()
Returns
Observable
: of the data
prop from the Region component.
streamProps
Helper function, for composing your props inside observe
, and then generating and returning an single Observable
.
Arguments
defaultProps
(Object
[optional]): Default props to start with.
Returns
Streamer
instance that implements these methods below:
All set*
methods return the same Streamer
instance so multiple set calls can be chained in one go.
set
set(key, value)
set(plainObject)
set(observable$, ...mapperFunctions)
setKey
setKey('key', 'value')
setPlainObject
setPlainObject({ key: 'value' })
setObservable
setObservable(observable$, ...mapperFunctions)
You can set as many mapper functions until you reach a value of your needs.
setObservable(
observable$,
props => props,
propsAgain => modifiedProps
)
setDispatch
setDispatch(actionCreators, store)
setDispatch({
incrementCounter: incrementCounter,
decrementCounter: decrementCounter,
}, store)
get$
get$()
Returns an Observable
.