mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
New getting started guide (WIP) in experimental section of docs (#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
This commit is contained in:
parent
3bde5bd0cb
commit
558f0d5042
@ -12,7 +12,7 @@ Getting started
|
||||
:maxdepth: 2
|
||||
|
||||
Installing the SDK <getting-started/installation>
|
||||
getting-started/quickstart
|
||||
Quickstart Guide <getting-started/quickstart>
|
||||
|
||||
Writing DAML
|
||||
------------
|
||||
@ -97,6 +97,7 @@ Experimental features
|
||||
daml-script/index
|
||||
tools/visual
|
||||
daml2ts/index
|
||||
getting-started/index
|
||||
|
||||
Support and updates
|
||||
-------------------
|
||||
|
105
docs/source/getting-started/app-architecture.rst
Normal file
105
docs/source/getting-started/app-architecture.rst
Normal file
@ -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
docs/source/getting-started/code/daml/User.daml
Normal file
45
docs/source/getting-started/code/daml/User.daml
Normal file
@ -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
docs/source/getting-started/code/ui-after/Feed.tsx
Normal file
27
docs/source/getting-started/code/ui-after/Feed.tsx
Normal file
@ -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
docs/source/getting-started/code/ui-after/MainView.tsx
Normal file
138
docs/source/getting-started/code/ui-after/MainView.tsx
Normal file
@ -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
docs/source/getting-started/code/ui-after/MessageEdit.tsx
Normal file
60
docs/source/getting-started/code/ui-after/MessageEdit.tsx
Normal file
@ -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;
|
100
docs/source/getting-started/code/ui-before/MainView.tsx
Normal file
100
docs/source/getting-started/code/ui-before/MainView.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
// 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';
|
||||
import { User } from '@daml2ts/create-daml-app/lib/create-daml-app-0.1.0/User';
|
||||
|
||||
const MainView: React.FC = () => {
|
||||
// -- HOOKS_BEGIN
|
||||
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);
|
||||
// -- HOOKS_END
|
||||
const reload = useReload();
|
||||
|
||||
const [exerciseAddFriend] = useExerciseByKey(User.AddFriend);
|
||||
const [exerciseRemoveFriend] = useExerciseByKey(User.RemoveFriend);
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default MainView;
|
143
docs/source/getting-started/first-feature.rst
Normal file
143
docs/source/getting-started/first-feature.rst
Normal file
@ -0,0 +1,143 @@
|
||||
.. Copyright (c) 2020 The DAML Authors. All rights reserved.
|
||||
.. SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
Your First Feature: Messaging Friends
|
||||
*************************************
|
||||
|
||||
Let's dive into implementing a feature for our social network app.
|
||||
From that we'll get a better idea of how to build DAML applications using our template.
|
||||
|
||||
Right now our app allows us to add and remove friends, but we can't communicate with them!
|
||||
Let's fix that by adding a private messaging feature.
|
||||
We will allow a user to send messages to a number of friends at once, and see all the messages that have been sent to them.
|
||||
Of course we must make sure that no one can see messages that were not sent to them.
|
||||
We will see that DAML lets us implement this in a direct and intuitive way.
|
||||
|
||||
There are two parts to building the messaging feature: the DAML code and the UI.
|
||||
Let's start with adding to the DAML code, on which we will base our UI changes.
|
||||
|
||||
DAML Changes
|
||||
============
|
||||
|
||||
The DAML code defines the *workflow* of the application.
|
||||
This means: what interactions between users (or *parties*) are permitted by the system?
|
||||
In the context of our feature, the question is: when is a user allowed to message another user?
|
||||
|
||||
The approach we'll take is: a user Bob can message another user Alice if Alice has added Bob as a friend.
|
||||
Remember that friendships are single-directional in our app.
|
||||
So Alice adding Bob as a friend means that she gives permission (or *authority*) for Bob to send her a message.
|
||||
|
||||
In DAML this workflow is represented as a new choice on the ``User`` contract.
|
||||
|
||||
.. literalinclude:: code/daml/User.daml
|
||||
:language: daml
|
||||
:start-after: -- SENDMESSAGE_BEGIN
|
||||
:end-before: -- SENDMESSAGE_END
|
||||
|
||||
Let's break this down.
|
||||
The choice is ``nonconsuming`` because sending a message should not affect the existence of the ``User`` contract.
|
||||
By convention, the choice returns the ``ContractId`` of the resulting ``Message`` contract (which we'll show next).
|
||||
Next, the parameters to the choice are the sender (the party wishing to talk to the signatory of this ``User`` contract) and the message text.
|
||||
The ``controller`` clause suggests that it is the ``sender`` who can exercise the choice.
|
||||
Finally, the body of the choice simply creates the new ``Message`` with the sender, receiver and content.
|
||||
|
||||
Note that there is no explicit check in the choice that the ``sender`` is a friend of the user.
|
||||
This is because the ``User`` contract is only ever visible to friends (the observers of the contract).
|
||||
|
||||
Now let's see the ``Message`` contract template.
|
||||
This is very simple - data and no choices - as well as the ``signatory`` declaration.
|
||||
|
||||
.. literalinclude:: code/daml/User.daml
|
||||
:language: daml
|
||||
:start-after: -- MESSAGE_BEGIN
|
||||
:end-before: -- MESSAGE_END
|
||||
|
||||
Note that we have two signatories on the ``Message`` contract: both the sender and receiver.
|
||||
This enforces the fact that the contract creation (and archival) must be authorized by both parties.
|
||||
|
||||
Now we've specified the workflow of sending messages, let's integrate the functionality into our app.
|
||||
|
||||
TypeScript Code Generation
|
||||
==========================
|
||||
|
||||
Remember that we interface with our DAML code from the UI components using the generated TypeScript.
|
||||
Since we have changed our DAML code, we also need to rerun the TypeScript code generator.
|
||||
Let's do this now by running::
|
||||
|
||||
daml build
|
||||
daml codegen ts .daml/dist/create-daml-app-0.1.0.dar -o daml-ts/src
|
||||
|
||||
As the TypeScript code is generated into the separate ``daml-ts`` workspace on which the UI depends, we need to rebuild the workspaces from the root directory using::
|
||||
|
||||
yarn workspaces run build
|
||||
|
||||
We should now have the updated TypeScript code with equivalents of the ``Message`` template and ``SendMessage`` choice.
|
||||
|
||||
Now let's implement our messaging feature in the UI!
|
||||
|
||||
Messaging UI
|
||||
============
|
||||
|
||||
Our messaging feature has two parts: a form with inputs for selecting friends and composing the message text, and a "feed" of messages that have been sent to you.
|
||||
Both parts will be implemented as React components that render on the main screen.
|
||||
|
||||
Feed Component
|
||||
--------------
|
||||
|
||||
The feed component is fairly straight-forward: it queries all ``Message`` contracts and displays their contents as a list.
|
||||
Here is the code for the entire component.
|
||||
|
||||
.. literalinclude:: code/ui-after/Feed.tsx
|
||||
|
||||
The key point here is that for any particular user, the ``Message`` query yields exactly the messages that have been either written by or sent to that user.
|
||||
This is due to how we modelled the signatories and observers in the ``Message`` template, and means we do not risk a privacy breach coming from the application code.
|
||||
|
||||
Message Edit Component
|
||||
----------------------
|
||||
|
||||
In addition to the feed component, we need a component for composing messages and sending them using the appropriate choice on the ``User`` contract.
|
||||
|
||||
.. literalinclude:: code/ui-after/MessageEdit.tsx
|
||||
|
||||
In this component we use React hooks for the message content and receiver.
|
||||
You can see these used in the ``submitMessage`` function, called when the "Send" button is clicked.
|
||||
The ``isSubmitting`` state is used to ensure that message requests are processed one at a time.
|
||||
The result of each send is a new ``Message`` contract created, after which the form is cleared.
|
||||
|
||||
View Component
|
||||
--------------------
|
||||
|
||||
The ``MainView`` component is the workhorse of this application which queries the ledger for data and houses the different subcomponents (e.g. friends, the network and our messaging components above).
|
||||
To support the messaging components, we will need DAML React hooks for querying ``Message`` contracts and exercising the ``SendMessage`` choice on our ``User`` contract.
|
||||
|
||||
First import the generated Typescript code for the ``Message`` contract template, as well as our two new components.
|
||||
|
||||
.. literalinclude:: code/ui-after/MainView.tsx
|
||||
:start-after: -- IMPORTS_BEGIN
|
||||
:end-before: -- IMPORTS_END
|
||||
|
||||
Then we declare the hooks themselves at the start of the component.
|
||||
|
||||
.. literalinclude:: code/ui-after/MainView.tsx
|
||||
:start-after: -- HOOKS_BEGIN
|
||||
:end-before: -- HOOKS_END
|
||||
|
||||
The ``messagesResult`` tracks the state of ``Message`` contracts on the ledger, where we specify no restrictions on the query.
|
||||
We extract the actual message data in ``messages``.
|
||||
The ``exerciseSendMessage`` hook gives us a function to exercise the appropriate choice on our ``User``.
|
||||
We wrap this in another ``sendMessage`` function which splits an input string into a list of parties and then exercises the choice, reporting to the user in the case of an error.
|
||||
|
||||
.. literalinclude:: code/ui-after/MainView.tsx
|
||||
:start-after: -- SENDMESSAGE_BEGIN
|
||||
:end-before: -- SENDMESSAGE_END
|
||||
|
||||
Finally we can integrate our new messaging components into the main screen view.
|
||||
In another segment we add the panel including our two new components: the ``MessageEdit`` and the ``Feed``.
|
||||
|
||||
.. literalinclude:: code/ui-after/MainView.tsx
|
||||
:start-after: -- MESSAGES_SEGMENT_BEGIN
|
||||
:end-before: -- MESSAGES_SEGMENT_END
|
||||
|
||||
You have now finished implementing your first end-to-end DAML feature!
|
||||
Let's give the new functionality a spin.
|
||||
We follow the instructions in "Running the app" to start up the new app.
|
96
docs/source/getting-started/index.rst
Normal file
96
docs/source/getting-started/index.rst
Normal file
@ -0,0 +1,96 @@
|
||||
.. Copyright (c) 2020 The DAML Authors. All rights reserved.
|
||||
.. SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.. _new-quickstart:
|
||||
|
||||
Full Stack DAML
|
||||
###############
|
||||
|
||||
**Disclaimer:** This guide is being actively developed.
|
||||
Expect major changes to the tutorial text and minor changes to the template application.
|
||||
|
||||
The goal of this tutorial is to get you up and running with full-stack, DAML-driven app development. We will provide you with the template for a miniature social networking app, get you writing your first end-to-end feature and finally deploy your new app to a persistent ledger.
|
||||
|
||||
By the end of the tutorial, you should have an idea of what DAML contracts and ledgers are, how the UI interacts with them, and how you might solve a potential use case with a DAML solution. We do not aim to give a comprehensive guide to all DAML concepts and tools; for that, see the later sections of the documentation. With that, let's get started!
|
||||
|
||||
.. TODO: reference specific sections of docs instead of saying "later sections".
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
|
||||
app-architecture
|
||||
first-feature
|
||||
|
||||
Prerequisites
|
||||
*************
|
||||
|
||||
If you haven't already, see :doc:`installation` for the DAML SDK and VSCode development environment.
|
||||
|
||||
You will also need some common software tools to build and interact with the template project.
|
||||
|
||||
- `Git <https://git-scm.com/>`_ version control system
|
||||
- `Yarn <https://yarnpkg.com/>`_ package manager for Javascript
|
||||
- A terminal application for command line interaction
|
||||
|
||||
|
||||
Running the app
|
||||
***************
|
||||
|
||||
We'll start by getting the app up and running, and then explain the different components which we will later extend.
|
||||
|
||||
First off, open a terminal, clone the template project and move to the project folder::
|
||||
|
||||
git clone https://github.com/digital-asset/create-daml-app.git
|
||||
cd create-daml-app
|
||||
|
||||
In order to connect the DAML model to the UI code, we need to run a code generation step (more on this later)::
|
||||
|
||||
daml build
|
||||
daml codegen ts .daml/dist/create-daml-app-0.1.0.dar -o daml-ts/src
|
||||
|
||||
Now, use Yarn to install the project dependencies and build the app::
|
||||
|
||||
yarn install
|
||||
yarn workspaces run build
|
||||
|
||||
You should see ``Compiled successfully.`` in the output if everything is working as expected.
|
||||
|
||||
.. TODO: Give instructions for possible failures.
|
||||
|
||||
We can now run the app in two steps.
|
||||
You'll need two terminal windows running for this.
|
||||
|
||||
In one terminal, at the root of the ``create-daml-app`` directory, run the script::
|
||||
|
||||
./daml-start.sh
|
||||
|
||||
This compiles the DAML component of the project and starts a *Sandbox* ledger for the app.
|
||||
The ledger in this case is stored in the Sandbox application memory; it is not persistent but is useful for testing and development.
|
||||
We'll leave the Sandbox running to serve requests from the UI, which result in changes to the in-memory ledger.
|
||||
|
||||
In a second terminal, navigate to the ``create-daml-app/ui`` folder and run::
|
||||
|
||||
yarn start
|
||||
|
||||
This starts the UI application connected to the already running Sandbox.
|
||||
The command should automatically open a window in your default browser at http://localhost:3000.
|
||||
If it doesn't, just open that link in any web browser.
|
||||
You may be asked whether to allow the app to receive network connections, which you should allow.
|
||||
|
||||
At this point you should see the login page for the social network.
|
||||
|
||||
.. TODO: Screenshot
|
||||
|
||||
Enter a user name of your choice and click the calculator icon next to the password field to generate a password token.
|
||||
(We do not have proper authentication in this app for simplicity.)
|
||||
Once you click "Sign up", you can see a screen with panels for your friends and the entire social network.
|
||||
Initially these are both empty as you don't have any friends yet!
|
||||
Go ahead and add some using the form (and remove them using the cross icons if you change your mind).
|
||||
|
||||
Now let's grow the network. Log out and sign up using the name of one of your friends.
|
||||
Let's say your name is Alice and your friend's name is Bob.
|
||||
Bob should now see Alice in the network (since she added him as a friend) and he is able to add her back.
|
||||
Note that in this app, friendships can be added in one direction at a time, similar to how "followers" work in Twitter.
|
||||
|
||||
Add a few more friends as Bob, and play around a bit more by logging in as different users and adding/removing friends from the network.
|
||||
This should give you a idea of the app's functionality.
|
@ -18,7 +18,7 @@ DAML SDK documentation
|
||||
:caption: Getting started
|
||||
|
||||
Installing the SDK <getting-started/installation>
|
||||
getting-started/quickstart
|
||||
Quickstart Guide <getting-started/quickstart>
|
||||
|
||||
.. toctree::
|
||||
:titlesonly:
|
||||
@ -102,6 +102,7 @@ DAML SDK documentation
|
||||
DAML Script <daml-script/index>
|
||||
tools/visual
|
||||
daml2ts/index
|
||||
getting-started/index
|
||||
|
||||
.. toctree::
|
||||
:titlesonly:
|
||||
|
Loading…
Reference in New Issue
Block a user