Skip to content

Commit

Permalink
Add Angular Material Card component (#1082)
Browse files Browse the repository at this point in the history
* Add Angular Material Card component

This component consists of the following components:

- card
- card_header
- card_content
- card_actions

I did not include card_footer since I couldn't tell what benefit it
added, but we can added later if needed.

I did not include an card_image component since it was easy enough to
use image with width 100%.

Closes #1080
  • Loading branch information
richard-to authored Nov 3, 2024
1 parent 18b7c3d commit 7a54883
Show file tree
Hide file tree
Showing 38 changed files with 592 additions and 0 deletions.
103 changes: 103 additions & 0 deletions demo/card.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import mesop as me


def load(e: me.LoadEvent):
me.set_theme_mode("system")


@me.page(
on_load=load,
security_policy=me.SecurityPolicy(
allowed_iframe_parents=["https://google.github.io"],
),
path="/card",
)
def app():
with me.box(
style=me.Style(
display="flex",
flex_direction="column",
gap=15,
margin=me.Margin.all(15),
max_width=500,
)
):
with me.card(appearance="outlined"):
me.card_header(
title="Grapefruit",
subtitle="Kind of fruit",
image="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
)
me.image(
style=me.Style(
width="100%",
),
src="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
)
with me.card_content():
me.text(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed augue ultricies, laoreet nunc eget, ultricies augue. In ornare bibendum mauris vel sodales. Donec ut interdum felis. Nulla facilisi. Morbi a laoreet turpis, sed posuere arcu. Nam nisi neque, molestie vitae euismod eu, sollicitudin eu lectus. Pellentesque orci metus, finibus id faucibus et, ultrices quis dui. Duis in augue ac metus tristique lacinia."
)

with me.card_actions(align="end"):
me.button(label="Add to cart")
me.button(label="Buy")

with me.card(appearance="raised"):
me.card_header(
title="Grapefruit",
subtitle="Kind of fruit",
image="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
image_type="small",
)

with me.card_content():
me.text(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed augue ultricies, laoreet nunc eget, ultricies augue. In ornare bibendum mauris vel sodales. Donec ut interdum felis. Nulla facilisi. Morbi a laoreet turpis, sed posuere arcu. Nam nisi neque, molestie vitae euismod eu, sollicitudin eu lectus. Pellentesque orci metus, finibus id faucibus et, ultrices quis dui. Duis in augue ac metus tristique lacinia."
)

with me.card_actions(align="start"):
me.button(label="Add to cart")
me.button(label="Buy")

with me.card(appearance="outlined"):
me.card_header(
title="Grapefruit",
subtitle="Kind of fruit",
image="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
image_type="medium",
)

with me.card_content():
me.text(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed augue ultricies, laoreet nunc eget, ultricies augue. In ornare bibendum mauris vel sodales. Donec ut interdum felis. Nulla facilisi. Morbi a laoreet turpis, sed posuere arcu. Nam nisi neque, molestie vitae euismod eu, sollicitudin eu lectus. Pellentesque orci metus, finibus id faucibus et, ultrices quis dui. Duis in augue ac metus tristique lacinia."
)

with me.card_actions(align="start"):
me.button(label="Add to cart")
me.button(label="Buy")

me.card_header(
title="Grapefruit",
image="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
image_type="large",
)

with me.card_content():
me.text(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed augue ultricies, laoreet nunc eget, ultricies augue. In ornare bibendum mauris vel sodales. Donec ut interdum felis. Nulla facilisi. Morbi a laoreet turpis, sed posuere arcu. Nam nisi neque, molestie vitae euismod eu, sollicitudin eu lectus. Pellentesque orci metus, finibus id faucibus et, ultrices quis dui. Duis in augue ac metus tristique lacinia."
)

with me.card_actions(align="end"):
me.button(label="Add to cart")
me.button(label="Buy")

me.card_header(
title="Grapefruit",
image_type="large",
)

with me.card_content():
me.text(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed augue ultricies, laoreet nunc eget, ultricies augue. In ornare bibendum mauris vel sodales. Donec ut interdum felis. Nulla facilisi. Morbi a laoreet turpis, sed posuere arcu. Nam nisi neque, molestie vitae euismod eu, sollicitudin eu lectus. Pellentesque orci metus, finibus id faucibus et, ultrices quis dui. Duis in augue ac metus tristique lacinia."
)
2 changes: 2 additions & 0 deletions demo/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import box as box
import button as button
import button_toggle as button_toggle
import card as card
import chat as chat
import chat_inputs as chat_inputs
import checkbox as checkbox
Expand Down Expand Up @@ -183,6 +184,7 @@ class Section:
name="Visual",
examples=[
Example(name="badge"),
Example(name="card"),
Example(name="divider"),
Example(name="icon"),
Example(name="progress_bar"),
Expand Down
18 changes: 18 additions & 0 deletions docs/components/card.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## Overview

Card is based on the [Angular Material card component](https://material.angular.io/components/card/overview).

## Examples

<iframe class="component-demo" src="https://google.github.io/mesop/demo/?demo=card" style="height: 200px"></iframe>

```python
--8<-- "demo/card.py"
```

## API

::: mesop.components.card.card.card
::: mesop.components.card_content.card_content.card_header
::: mesop.components.card_content.card_content.card_content
::: mesop.components.card_actions.card_actions.card_actions
4 changes: 4 additions & 0 deletions mesop/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ py_library(
deps = [
":version",
# REF(//scripts/scaffold_component.py):insert_component_import
"//mesop/components/card_header:py",
"//mesop/components/card_actions:py",
"//mesop/components/card_content:py",
"//mesop/components/card:py",
"//mesop/components/button_toggle:py",
"//mesop/components/date_range_picker:py",
"//mesop/components/datepicker:py",
Expand Down
8 changes: 8 additions & 0 deletions mesop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@
from mesop.components.button_toggle.button_toggle import (
button_toggle as button_toggle,
)
from mesop.components.card.card import card as card
from mesop.components.card_actions.card_actions import (
card_actions as card_actions,
)
from mesop.components.card_content.card_content import (
card_content as card_content,
)
from mesop.components.card_header.card_header import card_header as card_header
from mesop.components.checkbox.checkbox import (
CheckboxChangeEvent as CheckboxChangeEvent,
)
Expand Down
9 changes: 9 additions & 0 deletions mesop/components/card/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
load("//mesop/components:defs.bzl", "mesop_component")

package(
default_visibility = ["//build_defs:mesop_internal"],
)

mesop_component(
name = "card",
)
Empty file.
3 changes: 3 additions & 0 deletions mesop/components/card/card.ng.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<mat-card [appearance]="getAppearance()" [style]="getStyle()">
<ng-content></ng-content>
</mat-card>
7 changes: 7 additions & 0 deletions mesop/components/card/card.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
syntax = "proto2";

package mesop.components.card;

message CardType {
optional string appearance = 1;
}
33 changes: 33 additions & 0 deletions mesop/components/card/card.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import Literal

import mesop.components.card.card_pb2 as card_pb
from mesop.component_helpers import (
Style,
insert_composite_component,
register_native_component,
)


@register_native_component
def card(
*,
appearance: Literal["outlined", "raised"] = "outlined",
style: Style | None = None,
key: str | None = None,
):
"""
This function creates a card.
Args:
appearance: Card appearance style: outlined or raised.
style: Style for the component.
key: The component [key](../components/index.md#component-key).
"""
return insert_composite_component(
key=key,
type_name="card",
style=style,
proto=card_pb.CardType(
appearance=appearance,
),
)
40 changes: 40 additions & 0 deletions mesop/components/card/card.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {MatCardModule} from '@angular/material/card';
import {Component, Input} from '@angular/core';
import {
Key,
Type,
Style,
} from 'mesop/mesop/protos/ui_jspb_proto_pb/mesop/protos/ui_pb';
import {CardType} from 'mesop/mesop/components/card/card_jspb_proto_pb/mesop/components/card/card_pb';
import {formatStyle} from '../../web/src/utils/styles';

@Component({
selector: 'mesop-card',
templateUrl: 'card.ng.html',
standalone: true,
imports: [MatCardModule],
})
export class CardComponent {
@Input({required: true}) type!: Type;
@Input() key!: Key;
@Input() style!: Style;
private _config!: CardType;

ngOnChanges() {
this._config = CardType.deserializeBinary(
this.type.getValue() as unknown as Uint8Array,
);
}

config(): CardType {
return this._config;
}

getStyle(): string {
return formatStyle(this.style);
}

getAppearance(): 'outlined' | 'raised' {
return this.config().getAppearance() as 'outlined' | 'raised';
}
}
13 changes: 13 additions & 0 deletions mesop/components/card/e2e/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
load("//build_defs:defaults.bzl", "py_library")

package(
default_visibility = ["//build_defs:mesop_examples"],
)

py_library(
name = "e2e",
srcs = glob(["*.py"]),
deps = [
"//mesop",
],
)
1 change: 1 addition & 0 deletions mesop/components/card/e2e/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import card_app as card_app
25 changes: 25 additions & 0 deletions mesop/components/card/e2e/card_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import mesop as me


@me.page(path="/components/card/e2e/card_app")
def app():
with me.card(appearance="outlined"):
me.card_header(
title="Grapefruit",
subtitle="Kind of fruit",
image="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
)
me.image(
style=me.Style(
width="100%",
),
src="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
)
with me.card_content():
me.text(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed augue ultricies, laoreet nunc eget, ultricies augue. In ornare bibendum mauris vel sodales. Donec ut interdum felis. Nulla facilisi. Morbi a laoreet turpis, sed posuere arcu. Nam nisi neque, molestie vitae euismod eu, sollicitudin eu lectus. Pellentesque orci metus, finibus id faucibus et, ultrices quis dui. Duis in augue ac metus tristique lacinia."
)

with me.card_actions(align="end"):
me.button(label="Add to cart")
me.button(label="Buy")
30 changes: 30 additions & 0 deletions mesop/components/card/e2e/card_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {test, expect} from '@playwright/test';

test.describe('Card', () => {
test('renders card', async ({page}) => {
await page.goto('/components/card/e2e/card_app');
expect(
await page
.locator('mat-card')
.evaluate((el) => el.classList.contains('mat-mdc-card-outlined')),
).toBeTruthy();
expect(await page.locator('mat-card-title').textContent()).toContain(
'Grapefruit',
);
expect(await page.locator('mat-card-subtitle').textContent()).toContain(
'Kind of fruit',
);
expect(await page.locator('mat-card-content').textContent()).toContain(
'Lorem ipsum dolor sit amet',
);
expect(
await page
.locator('mat-card-actions')
.evaluate((el) =>
el.classList.contains('mat-mdc-card-actions-align-end'),
),
).toBeTruthy();
expect(await page.getByText('Add to cart')).toHaveCount(1);
expect(await page.getByText('Buy')).toHaveCount(1);
});
});
9 changes: 9 additions & 0 deletions mesop/components/card_actions/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
load("//mesop/components:defs.bzl", "mesop_component")

package(
default_visibility = ["//build_defs:mesop_internal"],
)

mesop_component(
name = "card_actions",
)
Empty file.
3 changes: 3 additions & 0 deletions mesop/components/card_actions/card_actions.ng.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<mat-card-actions [align]="getAlign()">
<ng-content></ng-content>
</mat-card-actions>
7 changes: 7 additions & 0 deletions mesop/components/card_actions/card_actions.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
syntax = "proto2";

package mesop.components.card_actions;

message CardActionsType {
optional string align = 1;
}
34 changes: 34 additions & 0 deletions mesop/components/card_actions/card_actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from typing import Literal

import mesop.components.card_actions.card_actions_pb2 as card_actions_pb
from mesop.component_helpers import (
insert_composite_component,
register_native_component,
)


@register_native_component
def card_actions(
*,
align: Literal["start", "end"],
key: str | None = None,
):
"""
This function creates a card_actions.
This component is meant to be used with the `card` component. It is used for the
bottom area of a card that contains action buttons.
This component is a optional. It is mainly used as a convenience for consistent
formatting with the card component.
Args:
align: Align elements to the left (start) or right (end).
"""
return insert_composite_component(
key=key,
type_name="card_actions",
proto=card_actions_pb.CardActionsType(
align=align,
),
)
Loading

0 comments on commit 7a54883

Please sign in to comment.