Live Apps

Make Quip's customers your customers.

Recipes

Want to add a new feature to your app and not sure how? Try one of these handy recipes. You can also read the Reference Docs.

How to Insert Your App

During the beta period, only developers of your app will be able to insert the app. To insert, type @ followed by the app name in a Quip document.

Others can view and interact with the app, but they won't be able to create new instances of it yet.

Storing Custom Data in Records

quip.apps.Record objects are the basic units of data storage in the Quip API.

The Getting Started guide is the best place to start learning about Records. The Record Reference Documentation explains in-depth exactly how to use Records, including RecordList, ImageRecord, and RichTextRecord.

For a simple real-life example, check out the Process Bar model.js code.

Subscribing to Changes

Responding promptly to data changes is what makes your live app feel alive. It's especially important to subscribe to changes in your Record data and update your app in response:

someRecord.listen(function(updatedRecord) {
    console.log("Record updated!");
    // Update app UI with updatedRecord
});

When the listen callback triggers for a Record, it means the Record's underlying data changed. That change might have come from the same user and window, or from another user or window.

You'll also want to listen on changes to recordList:

recordList.listen(function(updatedList) {
    console.log("Record list updated");
    // Update app UI with updatedList
});

When the listen callback triggers for a RecordList, it means that list items were added, moved, or removed.

The Record APIs and RecordList APIs have more details on listening to changes in quip.apps.Record and quip.apps.RecordList data. In addition, Subscribing to Events has a list of the environment updates (such as blur and focus) that we currently support, along with the generic interface you can use to subscribe/unsubscribe from these events.

Debugging Your App

Try these debugging flows when things go wrong.

  • Are you seeing a failed to load error?
    • Check the browser Javascript console for errors.
    • Check the output of npm start (this command should be continuously running).
  • Did you update manifest.json?

You can set up your Chrome Devtools console to invoke Javascript from within your App's iframe (e.g. quip.apps.getRootRecord(), which doesn't exist outside of the App's iframe). On the top-left of your console, click the "top" dropdown and select "v-0 (view-frame)".

Specifying the Size of Your App

Your app must choose one of three sizing_mode options, specified in the app manifest. These modes help your app behave well across many different documents and devices.

The options for sizing_mode are:

  • fill_container sets the app's width to fill the container, and adjusts height based on contents.
  • fit_content adjusts the app's width and height based on its contents, scrolling horizontally if needed.
  • scale sets an initial width, then shrinks proportionately when narrower.
    • Call quip.apps.enableResizing() during initialization to allow the user to dynamically scale the app (and quip.apps.disableResizing() to turn it off).

Your app manifest must also specify initial_height (and initial_width if not using fill_container) to determine the placeholder size when new instances of your app are created.

Additional options (see API reference for details)

  • fill_container_max_width sets a max width rather than always filling the container.
  • allow_tiny_size allows your app to have a very small width or height.

Adding Quip Comments

It's easy to support cross-platform Quip commenting in your app. To enable comments for a Record, you'll need to override two quip.apps.Record methods:

Then, add the CommentsTrigger React component and pass in the relevant record. This component renders a standard Quip comment bubble that automatically updates with the current number of comments.

Each Record can have its own Quip comment thread.

If you want to customize the appearance of the trigger, you can make your own custom trigger with these Record methods:

Configuring toolbar and context menus

Toolbar and context menus are an intuitive way to support customization of your app. Toolbar menus appear as buttons and dropdowns at the top of your app, while context menus can be triggered by right-click or custom buttons.

Get started with the examples in Menus Reference Documentation. For more advanced usage, browse the Example Apps to see how menus can be used to change colors, switch between modes, and more.

Mobile Behavior

People expect Quip documents to work great on mobile, and your app is no exception. The API makes detecting the client environment as simple as calling a function. Here's how you check if your app is being rendered on mobile:

if (quip.apps.isMobile()) {
    console.log("It's mobile!")
}

Your app can also optimize itself to behave differently in smaller containers. For example, try resizing the window after inserting the @Calendar app, or comparing how it looks on web versus a phone.

Here's how you can check the container width. You'll also want to listen on changes so your app can adjust dynamically:

if (quip.apps.getContainerWidth() < 600) {
    console.log("A wee bit small.")
}

quip.apps.addEventListener(
    quip.apps.EventType.CONTAINER_SIZE_UPDATE,
    function() {
        console.log("Container size updated! New width: " + quip.apps.getContainerWidth());
    }
);

Working With User Information

Your app can access the currently-viewing Quip user and all other users who can see the document.

Once you have a user object, you can call getId, getName, and getFirstName (see API documentation for details).

Your app can also display user profile pictures like this: <quip.apps.ui.ProfilePicture user={someUserObject} size={sizeInPx} round={true}/>.

Working With External Data Via OAuth

To illustrate how to work with external data, we will walk through an example of how to use the auth system to retrieve a list of files in the user's Google Drive.

First, add an OAUTH2 type auth configuration to your App. It should look something like this.

Name:
"gdrive"

Authorization URL:
"https://accounts.google.com/o/oauth2/v2/auth"

Token URL:
"https://www.googleapis.com/oauth2/v4/token"

Client ID:
"..."

Client Secret:
"..."

Scope:
"https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/drive.metadata.readonly"

Proxy API Domains:
"https://*.googleapis.com"

Then, copy the generated Redirect URL on auth configuration page and set it in the client app configuration on the third party platform.

Now, you can use the Auth API to trigger an auth flow and retrieve the a list of your your files from Google Drive.

quip.apps.auth("gdrive")
    .login({
        "access_type": "offline",
        "prompt": "consent",
    })
    .then(() => {
        return quip.apps.auth("gdrive").request({
            url: "https://www.googleapis.com/drive/files/root"
        }).then(response => response.json());
    })
    .catch(error => console.log("Auth failed", error));

Please see our auth documentation for more information on how to use the auth system.

Storing User Preferences

Most of your app's data will be stored in Records tied to specific app instances. But sometimes, you may need to store data associated with a user that is shared across different instances of your app.

User preferences allow your app to store user-specific data for the viewing user, accessible to your app only. All preference keys and values must be strings.

Save a preference:

var prefs = quip.apps.getUserPreferences();
prefs.save({favoriteDog: "Leo", favoriteArtist: "Carly Rae Jepsen");
prefs.save({favoriteColor: "teal"});

Only keys included in the prefs dictionary are modified — all other keys are preserved unless they are explicitly set to undefined.

Read a specific preference, then clear it:

var prefs = quip.apps.getUserPreferences();
var color = prefs.getForKey("favoriteColor");
prefs.save({favoriteColor: undefined});

Read all preferences, then clear all:

var prefs = quip.apps.getUserPreferences();
var all = prefs.getAll();
var faveArtist = all.favoriteArtist;
prefs.clear();

See Preferences for a full description of the APIs.

Create an ImageRecord using an Image URL

Quip's support for the user to select and upload an image from their device is great, but sometimes you want to get an image from the web and store it into an ImageRecord.

Here's how you can achieve it:

Get the image using fetch and store the result as a blob

function getImage(url) {
    return fetch(url)
    .then(response => {
        if (!response.ok) {
            throw Error(response.statusText);
        }
        return response.arrayBuffer();
        })
    .then(buffer => {
        return new Blob([new Uint8Array(buffer)]);
    });
}

Use ImageRecord's uploadFile method to store the data into the record:

const imageBlob = await getImage('https://image/url.png');
    imageRecord.uploadFile(imageBlob);

See ImageRecord for a full description of the APIs.

Add Floating Comments to Images

It's easy to add floating comments into your own images, just like Quip's standard @Image. ImageRecord natively supports a canvas that gives added transparently to support commenting.

First, create an ImageRecord that stores the image you like.

Render a quip.apps.ui.Image component that will display that record and that will store the node reference into a React variable - we'll need that to add comments in the right place. Here's an example of how you may want to render it:

<quip.apps.ui.Image
    record={imageRecord}
    responsiveToContainerWidth={true}
    allowResizing={false}
    onWidthAndAspectRatioUpdate={() => {}}
    ref={node => this.imageNode = node} />

Finally, you need to create a method to handle clicks on the image - the easiest way to do it is to wrap the quip.apps.ui.Image component with a div that triggers the method using the onClick event:

imageClickHandler = (e) => {
    this.imageNode.addCommentAtPoint(e.clientX, e.clientY);
}

// ... the rest of the component goes here

render() {
    return <div onClick={this.imageClickHandler}>
        <quip.apps.ui.Image ... />
    </div>;
}

Voilà! You can now click anywhere on the image and add a comment in the right place!

Here's the full snippet:

import React, { Component } from 'react';

    class ImageWithComments extends Component {
        imageNode = null

        imageClickHandler = (e) => {
            this.imageNode.addCommentAtPoint(e.clientX, e.clientY);
        }

        render() {
            const imageRecord = this.props.imageRecord;

            return (
                <div onClick={this.imageClickHandler}>
                    <quip.apps.ui.Image
                        record={imageRecord}
                        responsiveToContainerWidth={true}
                        allowResizing={false}
                        onWidthAndAspectRatioUpdate={() => {}}
                        ref={node => this.imageNode = node} />
                </div>
            );
        }
    }

    export default ImageWithComments;

See ImageRecord for a full description of the APIs.

Expand to Full Document Mode

Much like you can with Quip Spreadsheets, you can add an option to view your Live App at full screen. This will allow your users expanded and larger view of your app.

The key for full document mode is quip.apps.getViewportDimensions(), the complete code for the wrapper div looks like this:

function getImage(url) {
    return fetch(url)
    .then(response => {
        if (!response.ok) {
            throw Error(response.statusText);
        }
        return response.arrayBuffer();
        })
    .then(buffer => {
        return new Blob([new Uint8Array(buffer)]);
    });
}

And as a workaround for the missing resize event you can use:

function checkSize()
    {
        var dim = quip.apps.getViewportDimensions();
        var rect = quip.apps.getBoundingClientRect();
        wrapper.style.top = (-rect.top) + 'px';
        wrapper.style.left = (-rect.left) + 'px';
        wrapper.style.width = dim.width + 'px';
        wrapper.style.height = dim.height + 'px';
    };
    mxEvent.addListener(wrapper, 'mouseenter', checkSize);
    mxEvent.addListener(wrapper, 'click', checkSize);

See Sizing and Resizing for a full description of the APIs.

Questions?

Ask our team and the Quip developer community over at the Salesforce Stack Exchange.