Skip to content

Commit

Permalink
New getting started guide (WIP) in experimental section of docs (digi…
Browse files Browse the repository at this point in the history
…tal-asset#4548)

* Start drafting new quickstart guide

* Show how to run the app

* Very rough instructions for playing with the app

* Explain core of User template

* Start explaining UI code

* Example of daml react hook (useQuery for allUsers)

* Talk about daml2ts and start describing the new feature

* Start talking about DAML for posts feature

* Add ui file referenced in text

* Start describing changes to UI for Post feature

* Rework feature section wording as Messaging instead of Posts

* Write about additions to MainController

* Talk about new components before the view and controller (bottom up style)

* Describe additions to MainView (may change if we inline MainView into MainController)

* Adapt to create-daml-app removing MainController

* Fix code snippet and try to highlight tsx but fail

* Split guide into sections and rename some sections

* Improve start of app arch section

* Minor edits to code snippets and wording

* Update setup instructions with codegen step

* Update UI components in 'before' code

* Move and update section explaining TS codegen

* Copy in new DAML files

* Update UI code for messaging feature and some of the explanatory text

* Start reworking DAML feature section

* Redo DAML feature section

* Edit initial section

* Edit intro para of arch section

* Edit start of DAML explanation

* Edit template explanation

* Minor edit to UI explanation

* Improve wording of DAML explanation

* Rework sig/obs explanation

* Update User.daml file from create-daml-app and label AddFriend choice

* Explain AddFriend choice better

* Move new GSG to experimental features section

* Undo accidental change to vscode settings

changelog_begin
changelog_end

* Copyright headers

* Revert unwanted change

* Remove typescript highlighting which doesn't work

* Tweak explanation of code generation

* Remove driven
  • Loading branch information
rohanjr authored Feb 17, 2020
1 parent 3bde5bd commit 558f0d5
Show file tree
Hide file tree
Showing 10 changed files with 718 additions and 2 deletions.
3 changes: 2 additions & 1 deletion docs/configs/pdf/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Getting started
:maxdepth: 2

Installing the SDK <getting-started/installation>
getting-started/quickstart
Quickstart Guide <getting-started/quickstart>

Writing DAML
------------
Expand Down Expand Up @@ -97,6 +97,7 @@ Experimental features
daml-script/index
tools/visual
daml2ts/index
getting-started/index

Support and updates
-------------------
Expand Down
105 changes: 105 additions & 0 deletions docs/source/getting-started/app-architecture.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
.. Copyright (c) 2020 The DAML Authors. All rights reserved.
.. SPDX-License-Identifier: Apache-2.0
App Architecture
****************

In this section we'll look at the different components of our social network app.
The goal is to familiarise you enough to feel comfortable extending the code with a new feature in the next section.

There are two main components in the code - the DAML model and the React/TypeScript frontend - with generated TypeScript code to bridge the two.
Let's start by looking at the DAML model, as this sets the core logic of the application.

The DAML Model
==============

Using VSCode (or a code editor of your choice), navigate to the ``daml`` subdirectory.
There is a single DAML file called ``User.daml`` with the model for users of the app.

The core data is at the start of the ``User`` contract template.

.. literalinclude:: code/daml/User.daml
:language: daml
:start-after: -- MAIN_TEMPLATE_BEGIN
:end-before: -- MAIN_TEMPLATE_END

There are two important aspects here:

1. The data definition (a *schema* in database terms), describing the data stored with each user contract.
In this case it is an identifier for the user and their current list of friends.
Both fields use the built-in ``Party`` type which lets us use them in the following clauses.

2. The signatories and observers of the contract.
The signatories are the parties authorized to create new versions of the contract or archive the contract.
In this case only the user has those rights.
The observers are the parties who are able to view the contract on the ledger.
In this case all friends of a user are able to see the user contract.

Let's say what the ``signatory`` and ``observer`` clauses mean in our app more concretely.
A user Alice can see another user Bob in the network only when Alice is a friend in Bob's user contract.
For this to be true, Bob must have previously added Alice as a friend (i.e. updated his user contract), as he is the sole signatory on his user contract.
If not, Bob will be invisible to Alice.

We can see some concepts here that are central to DAML, namely *privacy* and *authorization*.
Privacy is about who can *see* what, and authorization is about who can *do* what.
In DAML we must answer these questions upfront, as they fundamentally change the design of an application.

The last thing we'll point out about the DAML model for now is the operation to add friends, called a *choice* in DAML.

.. literalinclude:: code/daml/User.daml
:language: daml
:start-after: -- ADDFRIEND_BEGIN
:end-before: -- ADDFRIEND_END

DAML contracts are *immutable* (can not be changed in place), so they must be updated by archiving the current instance and creating a new one.
That is what the ``AddFriend`` choice does: after checking some preconditions, it creates a new user contract with the new friend added to the list.
The ``choice`` syntax automatically includes the archival of the current instance.

.. TODO Update depending on consuming/nonconsuming choice.
Next we'll see how our DAML code is reflected and used on the UI side.

TypeScript Code Generation
==========================

The user interface for our app is written in `TypeScript <https://www.typescriptlang.org/>`_.
TypeScript is a variant of Javascript that provides more support in development through its type system.

In order to build an application on top of DAML, we need a way to refer to the DAML template and choices in TypeScript.
We do this using a DAML to TypeScript code generation tool in the DAML SDK.

To run code generation, we first need to compile the DAML model to an archive format (with a ``.dar`` extension).
Then the command ``daml codegen ts`` takes this file as argument to produce a number of TypeScript files in the specified location.

daml build
daml codegen ts .daml/dist/create-daml-app-0.1.0.dar -o daml-ts/src

We now have TypeScript types and companion objects in the ``daml-ts`` workspace which we can use from our UI code.
We'll see that next.

The UI
======

Our UI is written using `React <https://reactjs.org/>`_ and
React helps us write modular UI components using a functional style - a component is rerendered whenever one of its inputs changes - combined with a judicious use of global state.

We can see the latter in the way we handle ledger state throughout the application code.
For this we use a state management feature in React called `Hooks <https://reactjs.org/docs/hooks-intro.html>`_.
You can see the capabilities of the DAML React hooks in ``create-daml-app/ui/src/daml-react-hooks/hooks.ts``.
For example, we can query the ledger for all visible contracts (relative to a particular user), create contracts and exercise choices on contracts.

.. TODO Update location to view DAML react hooks API
Let's see some examples of DAML React hooks.

.. literalinclude:: code/ui-before/MainView.tsx
:start-after: -- HOOKS_BEGIN
:end-before: -- HOOKS_END

This is the start of the component which provides data from the current state of the ledger to the main screen of our app.
The three declarations within ``MainView`` all use DAML hooks to get information from the ledger.
For instance, ``allUsers`` uses a catch-all query to get the ``User`` contracts on the ledger.
However, the query respects the privacy guarantees of a DAML ledger: the contracts returned are only those visible to the currently logged in party.
This explains why you cannot see *all* users in the network on the main screen, only those who have added you as a friend (making you an observer of their ``User`` contract).

.. TODO You also see friends of friends; either explain or prevent this.
45 changes: 45 additions & 0 deletions docs/source/getting-started/code/daml/User.daml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
-- Copyright (c) 2020 The DAML Authors. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0

daml 1.2
module User where

-- MAIN_TEMPLATE_BEGIN
template User with
username: Party
friends: [Party]
where
signatory username
observer friends
-- MAIN_TEMPLATE_END

key username: Party
maintainer key

-- ADDFRIEND_BEGIN
choice AddFriend: ContractId User with
friend: Party
controller username
do
assertMsg "You cannot add yourself as a friend" (friend /= username)
assertMsg "You cannot add a friend twice" (friend `notElem` friends)
create this with friends = friend :: friends
-- ADDFRIEND_END

-- SENDMESSAGE_BEGIN
nonconsuming choice SendMessage: ContractId Message with
sender: Party
content: Text
controller sender
do
create Message with sender, receiver = username, content
-- SENDMESSAGE_END

-- MESSAGE_BEGIN
template Message with
sender: Party
receiver: Party
content: Text
where
signatory sender, receiver
-- MESSAGE_END
27 changes: 27 additions & 0 deletions docs/source/getting-started/code/ui-after/Feed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) 2020 The DAML Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import React from 'react'
import { List, ListItem } from 'semantic-ui-react';
import { Message } from '@daml2ts/create-daml-app/lib/create-daml-app-0.1.0/User';

type Props = {
messages: Message[];
}

/**
* React component to show a feed of messages for a particular user.
*/
const Feed: React.FC<Props> = ({messages}) => {
const showMessage = (message: Message): string => {
return (message.sender + ": " + message.content);
}

return (
<List relaxed>
{messages.map((message) => <ListItem>{showMessage(message)}</ListItem>)}
</List>
);
}

export default Feed;
138 changes: 138 additions & 0 deletions docs/source/getting-started/code/ui-after/MainView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Copyright (c) 2020 The DAML Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import React from 'react';
import { Container, Grid, Header, Icon, Segment, Divider } from 'semantic-ui-react';
import { Party } from '@daml/types';
import { useParty, useReload, useExerciseByKey, useFetchByKey, useQuery } from '@daml/react';
import UserList from './UserList';
import PartyListEdit from './PartyListEdit';
// -- IMPORTS_BEGIN
import { User, Message } from '@daml2ts/create-daml-app/lib/create-daml-app-0.1.0/User';
import MessageEdit from './MessageEdit';
import Feed from './Feed';
// -- IMPORTS_END

const MainView: React.FC = () => {
const username = useParty();
const myUserResult = useFetchByKey<User, Party>(User, () => username, [username]);
const myUser = myUserResult.contract?.payload;
const allUsersResult = useQuery<User, Party>(User);
const allUsers = allUsersResult.contracts.map((user) => user.payload);
const reload = useReload();

const [exerciseAddFriend] = useExerciseByKey(User.AddFriend);
const [exerciseRemoveFriend] = useExerciseByKey(User.RemoveFriend);

// -- HOOKS_BEGIN
const messagesResult = useQuery(Message, () => ({receiver: username}), []);
const messages = messagesResult.contracts.map((message) => message.payload);

const [exerciseSendMessage] = useExerciseByKey(User.SendMessage);
// -- HOOKS_END


const addFriend = async (friend: Party): Promise<boolean> => {
try {
await exerciseAddFriend(username, {friend});
return true;
} catch (error) {
alert("Unknown error:\n" + JSON.stringify(error));
return false;
}
}

const removeFriend = async (friend: Party): Promise<void> => {
try {
await exerciseRemoveFriend(username, {friend});
} catch (error) {
alert("Unknown error:\n" + JSON.stringify(error));
}
}

// -- SENDMESSAGE_BEGIN
const sendMessage = async (content: string, receiver: string): Promise<boolean> => {
try {
await exerciseSendMessage(receiver, {sender: username, content});
return true;
} catch (error) {
alert("Error while sending message:\n" + JSON.stringify(error));
return false;
}
}
// -- SENDMESSAGE_END

React.useEffect(() => {
const interval = setInterval(reload, 5000);
return () => clearInterval(interval);
}, [reload]);

return (
<Container>
<Grid centered columns={2}>
<Grid.Row stretched>
<Grid.Column>
<Header as='h1' size='huge' color='blue' textAlign='center' style={{padding: '1ex 0em 0ex 0em'}}>
{myUser ? `Welcome, ${myUser.username}!` : 'Loading...'}
</Header>

<Segment>
<Header as='h2'>
<Icon name='user' />
<Header.Content>
{myUser?.username ?? 'Loading...'}
<Header.Subheader>Me and my friends</Header.Subheader>
</Header.Content>
</Header>
<Divider />
<PartyListEdit
parties={myUser?.friends ?? []}
onAddParty={addFriend}
onRemoveParty={removeFriend}
/>
</Segment>
<Segment>
<Header as='h2'>
<Icon name='globe' />
<Header.Content>
The Network
<Icon
link
name='sync alternate'
size='small'
style={{marginLeft: '0.5em'}}
onClick={reload}
/>
<Header.Subheader>Others and their friends</Header.Subheader>
</Header.Content>
</Header>
<Divider />
<UserList
users={allUsers.sort((user1, user2) => user1.username.localeCompare(user2.username))}
onAddFriend={addFriend}
/>
</Segment>
// -- MESSAGES_SEGMENT_BEGIN
<Segment>
<Header as='h2'>
<Icon name='pencil square' />
<Header.Content>
Messages
<Header.Subheader>Send a message to a friend</Header.Subheader>
</Header.Content>
</Header>
<MessageEdit
sendMessage={sendMessage}
/>
<Divider />
<Feed messages={messages} />
</Segment>
// -- MESSAGES_SEGMENT_END
</Grid.Column>
</Grid.Row>
</Grid>
</Container>
);
}

export default MainView;
60 changes: 60 additions & 0 deletions docs/source/getting-started/code/ui-after/MessageEdit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) 2020 The DAML Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import React from 'react'
import { Form, Input, Button } from 'semantic-ui-react';
import { Text } from '@daml/types';

type Props = {
sendMessage: (content: Text, receiver: string) => Promise<boolean>;
}

/**
* React component to edit a message to send to a friend.
*/
const MessageEdit: React.FC<Props> = ({sendMessage}) => {
const [content, setContent] = React.useState('');
const [receiver, setReceiver] = React.useState('');
const [isSubmitting, setIsSubmitting] = React.useState(false);

const submitMessage = async (event?: React.FormEvent) => {
if (event) {
event.preventDefault();
}
setIsSubmitting(true);
const success = await sendMessage(content, receiver);
setIsSubmitting(false);
if (success) {
setContent('');
setReceiver('');
}
}

return (
<Form onSubmit={submitMessage}>
<Input
fluid
transparent
readOnly={isSubmitting}
loading={isSubmitting}
placeholder='Choose a friend'
value={receiver}
onChange={(event) => setReceiver(event.currentTarget.value)}
/>
<br />
<Input
fluid
transparent
readOnly={isSubmitting}
loading={isSubmitting}
placeholder="Write a message"
value={content}
onChange={(event) => setContent(event.currentTarget.value)}
/>
<br />
<Button type="submit">Send</Button>
</Form>
);
};

export default MessageEdit;
Loading

0 comments on commit 558f0d5

Please sign in to comment.