All Blog Posts

Displaying Content In React-Native

CharityKit is a CUBE product which aims to allow our clients to provide bespoke custom content to their users. How do you let clients customise content while keeping a coherent brand style? In this post, we look into how you can display content with React-Native.

10 min read

Development

Displaying Content With React Native

One of the main things we are aiming to achieve with CharityKit is the ability for charities to provide bespoke custom content to their users, tailored to both their charity’s brand and the current user. To do this we needed to think of a way we could allow charities to edit and customise the app’s content whilst keeping it looking nice and well-suited to the brand. We also wanted to allow content updates to come through to the device without submitting to the app store – Something we’ve done before (Storm), but believed we could do better and in a more customisable manner.

Please note, we won’t be discussing our delivery system here, as it’s native code that manages the downloading of new content and so doesn’t come under the scope of React Native so to speak.

The render function.

To display view structure on the screen, React Native boils down to a single Javascript function which returns a single child element written in JSX which is converted to React Native components (Note, you can return a root JavaScript component object, but JSX makes your views a lot more readable) to be rendered on screen:

render() {
    return (
        <View style={{flexDirection: 'row', height: 100, padding: 20}}>
            <View style={{backgroundColor: 'blue', flex: 0.3}} />
            <View style={{backgroundColor: 'red', flex: 0.5}} />
            <MyCustomComponent {...customProps} />
        </View>
    );
}

 

As you can see here, this very closely resembles an XML DOM structure (apart from the use of handlebars). For this reason – and also because, well, the whole internet seems to think HTML (a very similar markup language to XML) is good for content – it made sense for us to provide our own structure as XML to ReactNative. This would also allow us to operate at a higher level than the ReactNative JSX, to simplify the interface we expose to our CMS frontend, and to provide immediately useful custom components to charities that they can use straight off the bat.

This meant, that at the first iteration of our content, it looked something similar to this:

<View>
    <Text> Hello World </Text>
    <MyCustomComponent/>
</View>

 

Generating our DOM

Because we’re working in JavaScript we need a way to get our XML into a JavaScript object which we can manipulate and use in our code. We originally considered jumping into native to convert our XML string to an object structure in a vain attempt to avoid doing too much heavy lifting in – what has until recently been slow on devices – JavaScript, however after realising the benefits of writing this logic only once and not jumping between native code and JavaScript – and that recent jumps in device specs have meant JavaScript is a lot faster on mobile devices – we settled on a popular library called htmlparser2 which could return us DOM structure from a string of XML. This resulted in a simple but short function for getting our needed DOM:

export function parseXml(content) {
    return new Promise((resolve, reject) => {
        /* parse xml to dom object */
        let handler = new htmlparser.DomHandler(function (error, dom) {
            if (error) {
                reject(error);
            } else {
                resolve(dom[0]);
            }
        });
        let parser = new htmlparser.Parser(handler, {
            xmlMode: true,
            lowerCaseTags: true
        });
        parser.write(content);
        parser.done();
    });
}

 

Creating our React-Native elements

Because ReactNative does in-fact render ReactNative components rather than raw XML, we needed to come up with a way to convert an XML DOM to React Native components. Originally we started by looking into an XML transform language called XSLT believing we could use it and a plugin for babel to directly transform our XML structure to base ReactNative components in the form of JSX, which could be used in our Render function.

It very quickly became obvious to us that not only was this going to be impractical, slow, and difficult to implement, but that we could retain greater control and achieve the same thing simply by navigating the XML DOM and using ReactNative’s method:

React.createElement(component, props, children)

 

The first parameter passed to this function is a Component Class, so in order to use this createElement function we needed to have access to a component for each Tag we receive from the XML DOM. We made the decision to define these all in one file called our component manifest.

Component manifest

The component manifest is a map from XML tag names to React Components which looks like so:

import React from "react-native";
import ContentFeed from "../views/ContentFeed";
import ContentImage from "../views/ContentImage";
import TextManifest from "./textManifest";

const manifest = {
    feed: ContentFeed,
    image: ContentImage,
    text: React.Text,
    view: React.View,
    scrollview: React.ScrollView,
    ...TextManifest
};

export default manifest;

 

This defines a hard mapping between the content provided in our XML structure and React Native component classes which can be used in the createElement call.

This way of doing things means we can utilise the custom component nature of React Native without the charity having to construct complex interfaces entirely by themselves. It also means we have much more control over the styling of complex UI elements whilst still letting the charity plaster them all over their app if they so wish to do so.

View parser

Our view parser is a simple function for returning a root React Native element from a root XML tag:

export function parseView(element, index) {

    if (element.type === "text") {

        if (element.data.trim().length === 0) {
            return null;
        }
        return element.data;
    }

    let component = ComponentManifest[element.name];

    if (component) {

        let reactChildren = [];
​
        if (element.children) {
            reactChildren = element.children.map((child, index) => {
                return parseView(child, index);
            }).filter(child => {
                return !!child;
            });
        }
​
        let props = parseAttributes(element.attribs);
        props.key = index;
        return React.createElement(component, props, reactChildren);
    }

    return null;
}

 

State

We now have an almost complete picture of how to parse our content, however there are still some things missing. We – for one – don’t have any way of using custom variables within our content. Because of the way this content will be generated and used we need to be able to display things in the user’s feed such as:

  • Their name.
  • The amount they’ve donated to the charity.
  • The name of their fundraiser.

To do this we need to be able to access items in the app’s state from our XML structure. We implemented state access by allowing strings in our XML to have parameters from the state of our app using a handlebars style notation:

<View>
    <Text> Hello {user.name} </Text>
</View>

 

These can also be used in any parameters on our element:

<View>
    <Card color='{user.preferredColor}'> Hello {user.name} </Card>
</View>

 

But what if we want to not take these parameters from state, but take them from our content instead? When we started asking ourselves this question – A question we had because we wanted the content structure (XML) itself to be user independent – we decided we needed some supplementary data for our content! That’s when we decided to make the move to providing our content as JSON (kind of).

Content state

We now know we need to provide state for our content in some cases. To implement this, we wrapped our view content in a JSON structure, meaning we could reference either other parameters provided in the JSON or parameters stored in our redux state. This structure looks very similar to the below:

{
    "views": {
        "body": "<View><Card color='{user.preferredColor}'> Hello {user.name} </Card></View>"
    },
    "state": {
        "user": {
            "name": "Simon",
            "preferredColor": "red"
        }
    }
}

 

We then introduce a final step before providing our root React Native element to the app, where we loop through the element and its children, and replace anything in attributes or content contained in curly braces either from the "state" parameter of the content, or from app state. There is some logic to determine whether the parameter comes from the content JSON or redux, but that’s beyond the scope of this article. The main part of this process can be seen in the recursive function below, which takes an element and the combined app and content state as inputs:

export default function bindElement(element, state) {
    let boundProps = {};

    if (typeof element === "string") {
        return bindString(element, state);
    } else {
        Object.keys(element.props).forEach(propName => {
            const prop = element.props[propName];

            if (typeof prop === "string") {
                boundProps[propName] = bindString(prop, state);
            }
        });
    }

    boundProps["style"] = Object.assign({}, boundProps["style"]);

    return React.cloneElement(element, boundProps, element.props.children.map(child => bindElement(child, state)));
}

 

The bindString function simply finds and replaces strings contained in curly braces with their path in the state object provided to the function.

As a side, we also made the decision that the state provided by the content can be merged with our redux state under certain conditions. So if we believe the content state to be more fresh than our redux state we will update our local redux state.

Bringing it all together

So now we have dynamic, charity editable, content, but we can only display one page at a time and we don’t have any logic for changing between pages or linking elsewhere from our content. This is where the sheer brilliance of ReactNative comes into play. Because of it’s DOM-like structure, we can provide a Link element in our manifest that other tags in our XML can be wrapped in and cause them to link on tap to content!

The whole of this component looks like so:

const ContentLink = React.createClass({

    propTypes: {
        href: React.PropTypes.string.isRequired
    },

    _handleLink() {
        handleLink(this.props.href);
    },

    render() {

        return (
            <TouchableHighlight style={{flex:1}} onPress={this._handleLink}>
                <View style={{flex:1}}>
                    {this.props.children}
                </View>
            </TouchableHighlight>
        );
    }
});

export default ContentLink;

 

Now our content can look like so:

{
    "views": {
        "body": "<View><Link href='ExternalLink://{user.webPage}'><Text> Hello {user.name} </Text></Link><Link href='InternalLink://4'><Text> Go to Page 4 </Text></Link></View>",
    },
    "state": {
        "user": {
            "name": "Cube",
            "webPage": "http://www.3sidedcube.com"
        }
    }
}

 

and we can link both to external websites, and internal pages based on the prived schema!

The future?

We still have a lot to figure out when it comes to displaying content – in particular global styling, and style overrides – but we’re in a great position to allow charities to build amazing content and change it on the fly without going through a tedious app store review process. We have the control of not exposing the styling of compound Elements to them and the freedom for them to use ReactNative primitive views if they so wish.

Published on May 4, 2016, last updated on August 7, 2018

Give us the seal of approval!

Like what you read? Let us know.

0

Tags

Join us for a Mobile Disco

Bring your ideas to life...

Find Out More