Skip to content

Commit

Permalink
Merge pull request #128 from holochain/2023-09-28-filter-helper
Browse files Browse the repository at this point in the history
Implement `filter` helper and use it to suppress reactive variables for non-widget fields
  • Loading branch information
c12i committed Jan 29, 2024
2 parents d4f18bb + 19eec7a commit 3302bdb
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 6 deletions.
3 changes: 3 additions & 0 deletions src/templates/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ use serde_json::Value;

pub mod merge;
pub mod uniq_lines;
pub mod filter;

use merge::register_merge;
use uniq_lines::register_uniq_lines;
use filter::register_filter;

pub fn register_helpers<'a>(h: Handlebars<'a>) -> Handlebars<'a> {
let h = register_concat_helper(h);
Expand All @@ -19,6 +21,7 @@ pub fn register_helpers<'a>(h: Handlebars<'a>) -> Handlebars<'a> {
let h = register_pluralize_helpers(h);
let h = register_merge(h);
let h = register_uniq_lines(h);
let h = register_filter(h);

h
}
Expand Down
157 changes: 157 additions & 0 deletions src/templates/helpers/filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
use handlebars::{
Context, Handlebars, Helper, HelperDef, RenderContext, RenderError,
ScopedJson,
};
use serde_json::{json, Value, Map};

#[derive(Clone, Copy)]
pub struct FilterHelper;

/// A Handlebars helper to filter an iterable JSON value.
/// It receives the value to be filtered and a string containing the condition predicate,
/// then uses Handlebars' truthy logic to filter the items in the value.
/// It also supports the `#if` helper's `includeZero` optional parameter.
impl HelperDef for FilterHelper {
fn call_inner<'reg: 'rc, 'rc>(
&self,
h: &Helper<'reg, 'rc>,
r: &'reg Handlebars<'reg>,
_ctx: &'rc Context,
_rc: &mut RenderContext<'reg, 'rc>,
) -> Result<ScopedJson<'reg, 'rc>, RenderError> {
let mut params = h.params().iter();
let value = params
.next()
.ok_or(RenderError::new(
"Filter helper: Param not found for index 0; must be value to be filtered",
))?
.value();

let condition = params
.next()
.ok_or(RenderError::new("Filter helper: Param not found for index 1; must be string containing filter condition predicate"))?
.value()
.as_str()
.ok_or(RenderError::new("Filter helper: filter condition predicate must be a string"))?;

let include_zero = h
.hash_get("includeZero")
.and_then(|v| v.value().as_bool())
.unwrap_or(false);

// This template allows us to evaluate the condition according to
// Handlebars' available context/property logic, helper functions, and
// truthiness logic.
let template = format!(
"{}{}{}{}",
"{{#if ",
condition,
include_zero.then_some(" includeZero=true").unwrap_or(""),
"}}true{{else}}false{{/if}}"
);

match value {
Value::Array(items) => {
let mut filtered_array = vec![];
for item in items.iter() {
match r.render_template(&template, &item) {
Ok(s) => {
if s.as_str() == "true" {
filtered_array.push(item);
}
},
Err(e) => {
return Err(e);
}
}
}
Ok(ScopedJson::Derived(json!(filtered_array)))
},
Value::Object(object) => {
let mut filtered_object = Map::new();
for key in object.clone().keys() {
if let Some(v) = object.get(key) {
match r.render_template(&template, &v) {
Ok(s) => {
if s.as_str() == "true" {
filtered_object.insert(key.into(), v.clone());
}
},
Err(e) => {
return Err(e);
}
}
}
}
Ok(ScopedJson::Derived(json!(filtered_object)))
},
_ => Err(RenderError::new("Filter helper: value to be filtered must be an array or object"))
}
}
}

pub fn register_filter<'a>(mut h: Handlebars<'a>) -> Handlebars<'a> {
h.register_helper("filter", Box::new(FilterHelper));

h
}

#[cfg(test)]
mod tests {
use handlebars::Handlebars;
use serde_json::json;
use crate::templates::helpers::register_filter;

fn setup_handlebars<'a>() -> Handlebars<'a> {
let hbs = Handlebars::new();
let hbs = register_filter(hbs);
hbs
}

#[test]
fn respects_include_zero() {
let hbs = setup_handlebars();
let value = json!([0, 1, 0, 2, 0, 3, 0, 4, 0, 5]);
// The predicate filters out zeroes.
let template = "{{#each (filter this \"this\")}}{{this}}{{/each}}";
match hbs.render_template(&template, &value) {
Ok(s) => assert_eq!(s, "12345", "`filter` helper did not filter out falsy zero"),
Err(e) => panic!("{}", e)
}
// This predicate, however, does not.
let template = "{{#each (filter this \"this\" includeZero=true)}}{{this}}{{/each}}";
match hbs.render_template(&template, &value) {
Ok(s) => assert_eq!(s, "0102030405", "`filter` helper did not treat zero as truthy"),
Err(e) => panic!("{}", e)
}
}

#[test]
fn can_filter_object_by_value() {
let hbs = setup_handlebars();
let value = json!({"name": "Alice", "age": 24, "wild": false, "species": "iguana"});
// The predicate filters out the 'wild' property.
let template = "{{#each (filter this \"this\")}}{{@key}}: {{this}}, {{/each}}";
match hbs.render_template(&template, &value) {
Ok(s) => assert_eq!(s, "name: Alice, age: 24, species: iguana, ", "`filter` helper did not filter object key/value pairs by value"),
Err(e) => panic!("{}", e)
}
}


#[test]
fn can_filter_complex_value() {
let hbs = setup_handlebars();
let value = json!([
{"name": "Alice", "age": 24, "wild": true, "species": "iguana"},
{"name": "Bob", "age": 3, "wild": false, "species": "hamster"},
{"name": "Carol", "age": 1, "wild": true, "species": "octopus"}
]);
// The predicate filters out domestic animals.
let template = "{{#each (filter this \"wild\")}}{{name}} the {{species}} is {{age}}. {{/each}}";
match hbs.render_template(&template, &value) {
Ok(s) => assert_eq!(s, "Alice the iguana is 24. Carol the octopus is 1. ", "`filter` helper did not operate on a list full of complex values"),
Err(e) => panic!("{}", e)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ let {{camel_case field_name}}: Array<{{> (concat field_type.type "/type")}} | un
let errorSnackbar: Snackbar;
$: {{#each entry_type.fields}}{{#if widget}}{{camel_case field_name}}{{#unless @last}}, {{/unless}}{{/if}}{{/each}};
$: {{#each (filter entry_type.fields "widget")}}{{camel_case field_name}}{{#unless @last}}, {{/unless}}{{/each}};
$: is{{pascal_case entry_type.name}}Valid = true{{#each entry_type.fields}}{{#if widget}}{{#if (eq cardinality "single")}} && {{> (concat field_type.type "/" widget "/is-valid") variable_to_validate=(camel_case field_name) }}{{/if}}{{#if (eq cardinality "vector")}} && {{camel_case field_name}}.every(e => {{> (concat field_type.type "/" widget "/is-valid") variable_to_validate="e" }}){{/if}}{{/if}}{{/each}};
onMount(() => {
Expand All @@ -55,7 +55,7 @@ onMount(() => {
async function update{{pascal_case entry_type.name}}() {
const {{camel_case entry_type.name}}: {{pascal_case entry_type.name}} = {
const {{camel_case entry_type.name}}: {{pascal_case entry_type.name}} = {
{{#each entry_type.fields}}
{{#if widget}}
{{#if (eq cardinality "single") }}
Expand Down Expand Up @@ -86,7 +86,7 @@ async function update{{pascal_case entry_type.name}}() {
updated_{{snake_case entry_type.name}}: {{camel_case entry_type.name}}
}
});
dispatch('{{kebab_case entry_type.name}}-updated', { actionHash: updateRecord.signed_action.hashed.hash });
} catch (e) {
errorSnackbar.labelText = `Error updating the {{lower_case entry_type.name}}: ${e.data.data}`;
Expand All @@ -99,7 +99,7 @@ async function update{{pascal_case entry_type.name}}() {
</mwc-snackbar>
<div style="display: flex; flex-direction: column">
<span style="font-size: 18px">Edit {{pascal_case entry_type.name}}</span>

{{#each entry_type.fields}}
{{#if widget}}
<div style="margin-bottom: 16px">
Expand All @@ -108,7 +108,7 @@ async function update{{pascal_case entry_type.name}}() {
{{else}}
{{> Vec/edit/render field_name=field_name field_type=field_type widget=widget }}
{{/if}}

</div>

{{/if}}
Expand All @@ -121,7 +121,7 @@ async function update{{pascal_case entry_type.name}}() {
on:click={() => dispatch('edit-canceled')}
style="flex: 1; margin-right: 16px"
></mwc-button>
<mwc-button
<mwc-button
raised
label="Save"
disabled={!is{{pascal_case entry_type.name}}Valid}
Expand Down

0 comments on commit 3302bdb

Please sign in to comment.